如果需要获取一个 DOM 元素,传统的 JS 方式是:
<template>
<div class="person">
<h1>中国</h1>
<h2 id="city">北京</h2>
<h3>大兴区</h3>
<button @click="showDom">显示dom元素</button>
</div>
</template>
<script setup lang="ts" name="Person">
const showDom = () => {
console.log(document.getElementById('city'))
}
</script>
在这个简单示例中这样做没问题,但在正式项目中就可能存在一个问题。在 Vue 工程中,是通过一个根组件加载诸多子组件,所以很容易出现不同的组件中包含同样 id 的 HTML 标签,比如在根组件App.vue
中:
<template>
<!-- HTML -->
<h1 id="city">Hello World</h1>
<Person></Person>
</template>
现在再点击按钮输出的就是App.vue
中的<h1>
标签,因为它先加载。
为了避免这种情况出现,vue 提供一个ref
属性,结合ref
方法可以获取对应的 DOM 元素:
<template>
<div class="person">
<h1>中国</h1>
<h2 ref="city">北京</h2>
<h3>大兴区</h3>
<button @click="showDom">显示dom元素</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const city = ref()
const showDom = () => {
console.log(city.value)
}
</script>
并且 HTML 标签的ref
属性仅在当前组件中有效,不会存在前文描述的问题。
ref
属性不仅可以用于获取 HTML 标签对应的 DOM 元素,还可以获取 vue 组件的实例对象:
<template>
<!-- HTML -->
<h1 id="city">Hello World</h1>
<Person ref="person"></Person>
<button @click="showPerson">show person</button>
</template>
<script setup lang="ts" name="App">
import Person from './Person2.vue'
import { ref } from 'vue'
const person = ref()
const showPerson = () => {
console.log(person.value)
}
</script>
但默认情况下获取到的组件实例中不包含任何方法和数据,如果父组件要通过这种方式访问子组件的数据或方法,就需要在子组件中暴露:
<script setup lang="ts" name="Person">
import { ref } from 'vue'
const city = ref()
const showDom = () => {
console.log(city.value)
}
defineExpose({
showDom
})
</script>
此时父组件就可以调用子组件暴露的方法(或数据):
<script setup lang="ts" name="App">
import Person from './Person2.vue'
import { ref } from 'vue'
const person = ref()
const showPerson = () => {
console.log(person.value)
person.value.showDom()
}
</script>
vue3 如此设计,vue2 不需要暴露父组件就可以使用子组件的数据和方法。
自定义类型
用普通的 JS 方式定义一个对象:
<script setup lang="ts" name="Person">
const person = {
id: 1,
name: 'zhang san',
age: 20
}
</script>
这样做对类型没有任何限制,也就没有类型检查,会引发常见的属性命名错误或在其它组件中引用时 IDE 无法智能联想提示属性名等问题。
可以在文件src\types\index.ts
中定义一个接口用于描述这种类型:
export interface Person {
id: number;
name: string;
age: number;
}
在需要定义类型的代码中导入并使用这种类型进行约束:
<script setup lang="ts" name="Person">
import {Person} from '@/types'
const person: Person = {
id: 1,
name: 'zhang san',
age: 20
}
console.log(person)
</script>
在 VSCode 中导入类型可能报错:
这是因为在 TS 规范中,导入类型和数据或方法不同,需要使用type
进行标记:
import {type Person} from '@/types'
定义一个Person
类型的数组:
const persons: Array<Person> = [{
id: 1,
name: 'zhang san',
age: 20
}, {
id: 2,
name: 'lisi',
age: 18
}, {
id: 3,
name: 'wangwu',
age: 16
}]
Array<Person>
是使用泛型数组的方式定义类型。除了这种方式还可以:
const persons: Person[] = [{
id: 1,
name: 'zhang san',
age: 20
}, {
id: 2,
name: 'lisi',
age: 18
}, {
id: 3,
name: 'wangwu',
age: 16
}]
还可以将 Person 数组定义为一个自定义类型:
export type PersonList = Array<Person>;
使用自定义类型进行约束:
<script setup lang="ts" name="Person">
import { type Person, type PersonList } from '@/types'
// ...
const persons: PersonList = [{
id: 1,
name: 'zhang san',
age: 20
}, {
id: 2,
name: 'lisi',
age: 18
}, {
id: 3,
name: 'wangwu',
age: 16
}]
</script>
props
props
是定义在组件上的属性,父组件可以通过它传递信息给子组件:
<template>
<!-- HTML -->
<Person ref="person" msg="hello"></Person>
</template>
<script setup lang="ts" name="App">
import Person from './Person5.vue'
</script>
子组件需要通过方法defineProps
接收:
<template>
<div class="person">
<p>{{ msg }}</p>
</div>
</template>
<script setup lang="ts" name="Person">
import { defineProps } from 'vue';
defineProps(['msg'])
</script>
defineProps
接收一个数组,数组中的值就是子组件接收的props
的名称。与此同时,如上面展示的,接收到的属性可以直接在模版中使用。
defineProps
这种以defineXXX
方式命名的 vue 方法被称作宏函数,是不需要通过import
导入的,因此这里可以省略:
<script setup lang="ts" name="Person">
// import { defineProps } from 'vue';
defineProps(['msg'])
</script>
需要注意的是,如果传递的不是常量,是变量,需要使用:
定义props
属性:
<template>
<!-- HTML -->
<Person ref="person" msg="hello" :persons="persons"></Person>
</template>
<script setup lang="ts" name="App">
import Person from './Person5.vue'
const persons = [{
id: 1,
name: '张三',
age: 18
},{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}]
</script>
实际上就是通过
v-bind:xxx
定义了一个值是表达式的props
属性。
子组件接收和使用的方式是相似的:
<template>
<div class="person">
<p>{{ msg }}</p>
<ul>
<li v-for="person in persons" :key="person.id">
{{ person.name }}
</li>
</ul>
</div>
</template>
<script setup lang="ts" name="Person">
// import { defineProps } from 'vue';
defineProps(['msg','persons'])
</script>
如果需要在代码中使用 props
而非模版中,可以通过defineProps
的返回值:
const props = defineProps(['msg','persons'])
console.log(props.msg)
当然,也可以通过props
传递响应式数据:
<template>
<!-- HTML -->
<Person ref="person" msg="hello" :persons="persons"></Person>
</template>
<script setup lang="ts" name="App">
import Person from './Person6.vue'
import { reactive } from 'vue';
const persons = reactive([{
id: 1,
name: '张三',
age: 18
},{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}])
</script>
可以添加类型约束:
import {type PersonList} from '@/types'
const persons:PersonList = reactive([{
id: 1,
name: '张三',
age: 18
},{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}])
这样做不会报错,但不推荐,更推荐的方式是通过 vue 函数的泛型添加类型约束:
const persons = reactive<PersonList>([{
id: 1,
name: '张三',
age: 18
},{
id: 2,
name: '李四',
age: 19
},
{
id: 3,
name: '王五',
age: 20
}])
类似的,子组件(接收方)同样可以以泛型的方式添加类型约束:
<script setup lang="ts" name="Person">
import { type PersonList } from '@/types';
const props = defineProps<{
msg: string;
persons: PersonList;
}>()
console.log(props.msg)
</script>
值得注意的是,此时defineProps
方法的参数可以省略。
可以将某个props
设置为可选的:
const props = defineProps<{
msg?: string;
persons: PersonList;
}>()
此时父组件可以传也可以不传:
<Person ref="person" :persons="persons"></Person>
此时通常需要在父组件没有传递props
时提供一个默认值,可以使用withDefaults
函数:
<script setup lang="ts" name="Person">
import { type PersonList } from '@/types';
import { withDefaults } from 'vue';
const props = withDefaults(defineProps<{
msg?: string;
persons: PersonList;
}>(), {
msg: () => 'hello world',
})
console.log(props.msg)
</script>
withDefaults
函数的第二个方法接收一个对象,其属性可以指定对应props
的默认值,需要注意的是其值只能是一个匿名函数,返回值就是props
的默认值。
生命周期
vue2
为了对比 vue2 和 vue3 的生命周期,需要先创建一个 vue2 项目。
先安装 vue2 脚手架创建工具 vue-cli:
npm install -g @vue/cli
安装要求是 node.js 版本 >= 18 且 <= 22,推荐使用 22,推荐使用 node.js 版本管理工具 nvm 切换 node.js 的版本。
使用工具创建 vue2 项目:
vue create vue2_test
注意需要在出现提示后选择 vue2 项目。
如果运行命令提示找不到命令,需要通过
npm config get prefix
查看 node.js 的安装目录,然后确保其中包含一个vue.cmd
和vue.ps
文件,并确保这个目录在环境变量path
中。
创建一个简单的示例项目:
<template>
<div>
<h1>{{ sum }}</h1>
<button @click="add">sum++</button>
</div>
</template>
<script>
/* eslint-disable */
export default {
name: 'Person',
methods: {
add() {
this.sum++
}
},
data() {
return {
sum: 0
}
}
}
</script>
组件在内存中创建前后会调用的两个生命周期钩子:
// 创建前
beforeCreate() {
console.log('beforeCreate')
},
// 创建完成
created() {
console.log('created')
},
组件被挂载到父组件后才能在页面显示,挂载前后的生命周期钩子:
// 挂载前
beforeMount() {
console.log('beforeMount')
},
// 挂载完成
mounted() {
console.log('mounted')
},
如果组件的数据发生变化(比如这里点击按钮让响应式数据自增),会调用生命周期钩子:
// 更新前
beforeUpdate() {
console.log('beforeUpdate')
},
// 更新完成
updated() {
console.log('updated')
},
组件被销毁的前后会调用生命周期钩子:
// 销毁前
beforeDestroy() {
console.log('beforeDestroy')
},
// 销毁完成
destroyed() {
console.log('destroyed')
}
可以在父组件中编写一个逻辑,以触发子组件的销毁:
<template>
<div id="app">
<person v-if="show"/>
<button @click="destroyPerson">destroyPerson</button>
</div>
</template>
<script>
import Person from './components/Person.vue'
export default {
name: 'App',
components: {
Person
},
data() {
return {
show: true
}
},
methods: {
destroyPerson() {
this.show = false
}
}
}
</script>
vue3
与vue2
类似,同样创建一个简单示例:
<template>
<div class="person">
<h1>{{ sum }}</h1>
<button @click="add">sum++</button>
</div>
</template>
<script setup lang="ts" name="Person">
import {ref} from 'vue'
const sum = ref(0)
const add = () => {
sum.value++
}
</script>
与vue2
不同的是,vue3
中没有创建前和创建后的钩子,因为setup
方法本身就相当于创建过程,因此可以这样表示创建前和创建后的时刻:
<script setup lang="ts" name="Person">
import {ref} from 'vue'
console.log("创建前")
const sum = ref(0)
const add = () => {
sum.value++
}
console.log("创建后")
</script>
挂载前和挂载后的钩子:
onBeforeMount(() => {
console.log("挂在前")
})
onMounted(() => {
console.log("挂载后")
})
更新前和更新后的钩子:
onBeforeUpdate(() => {
console.log("更新前")
})
onUpdated(() => {
console.log("更新后")
})
组件卸载前后(相当于 vue2 的销毁前后,只是称呼不同)的生命周期钩子:
onBeforeUnmount(() => {
console.log("卸载前")
})
onUnmounted(() => {
console.log("卸载后")
})
同样的,利用v-if
在父组件触发子组件的销毁以观察卸载的生命周期钩子被触发:
<template>
<!-- HTML -->
<Person v-if="show"/>
<button @click="show=false">destroy person</button>
</template>
<script setup lang="ts" name="App">
import Person from './Person7.vue'
import { ref } from 'vue'
const show = ref(true)
</script>
最后需要说明的是,父子组件的加载顺序是先加载子组件,在子组件加载完毕后才加载父组件,而根组件最后完成加载。
hooks
看一个简单示例:
<template>
<div class="person">
<h1>{{ sum }}</h1>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<button @click="add">sum++</button>
<button @click="addList">list++</button>
</div>
</template>
<script setup lang="ts" name="Person">
import { ref, reactive } from 'vue'
const sum = ref(0)
const list = reactive<number[]>([])
const add = () => {
sum.value++
}
const addList = () => {
list.push(list.length + 1)
}
</script>
这个子组件中有两部分功能,一部分是让sum
自增,另一部分是展示一个从1
开始的等差数列。现在这个示例中两部分代码的数据和方法是混杂在一起的,可以通过 hooks
实现模块化封装。
将sum
自增部分的数据和方法抽取并封装到src\hooks\useList.ts
:
import { reactive } from "vue";
export default () => {
const list = reactive<number[]>([]);
const addList = () => {
list.push(list.length + 1);
};
return { list, addList };
};
可以看到,数据和方法都封装到一个匿名函数中,通过默认暴露的方式提供给外部调用,其数据和方法都以返回值的方式提供给调用方。
类似的,等差数列的部分代码封装到src\hooks\useSum.ts
:
import { ref } from "vue";
export default () => {
const sum = ref(0);
const add = () => {
sum.value++;
};
return { sum, add };
};
hooks 文件习惯以
useXXX
的方式命名。
重构子组件,导入两个 hooks 文件,并使用其中的方法结合解构赋值的方式引入数据和方法:
<template>
<div class="person">
<h1>{{ sum }}</h1>
<ul>
<li v-for="(item, index) in list" :key="index">{{ item }}</li>
</ul>
<button @click="add">sum++</button>
<button @click="addList">list++</button>
</div>
</template>
<script setup lang="ts" name="Person">
import useSum from './hooks/useSum'
import useList from './hooks/useList'
const {sum, add} = useSum()
const {list, addList} = useList()
</script>
这种方式的组织代码的好处是可以让相同功能的代码组织在一个 hooks 文件中,不仅包含数据和方法,如果有必要,还可以将相应的计算属性、监视、生命周期钩子都写在一起。比如默认就加载三个等差数列的元素:
import { reactive, onMounted } from "vue";
export default () => {
const list = reactive<number[]>([]);
const addList = () => {
list.push(list.length + 1);
};
onMounted(() => {
addList();
addList();
addList();
});
return { list, addList };
};
The End.
文章评论