一、前言
自2020年9月18号vue3发布以来,直到今年2月份,vue3成为默认版本,意味着vue正式进入了vue3 时代,作为前端大军的一员,当然要紧跟时代的潮流,将vue3玩转起来,虽然前期也断断续续学过vue3的知识,也做过一些项目,但是都没有形成文章记录起来,因此想通过一系列的文章系统的学习一下vue3的知识,并记录一些使用过程中的经验,文章内容还是基础为主,希望能对你有帮助。
二、defineComponent
我们知道,vue3的 一个很大的变化就是他全面支持typescript了,所以在使用了解和使用setup函数之前,我们先来简单了解一下defineComponent,它是vue3新推出的API,可以帮助我们做Typescript的类型推导,这样的话就可以简化掉很多我们在编写时的类型定义。比如,在没有defineComponent之时,我们使用setup函数之前,必须给他的参数手动声明类型:
import { Slots } from 'vue'
// 声明 props 和 return 的数据类型
interface Data {
[key: string]: unknown
}
// 声明 context 的类型
interface SetupContext {
attrs: Data
slots: Slots
emit: (event: string, ...args: unknown[]) => void
}
// 使用的时候入参要加上声明, return 也要加上声明
export default {
setup(props: Data, context: SetupContext): Data {
// ...
return {
// ...
}
},
}
可以看出,这样的话就太繁琐了,但是有了defineComponent之后,我们使用setup函数就简单多了:
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
// ...
return {
// ...
}
},
})
有了他之后我们就可以只需要声明自自己声明的变量的类型,专注于业务了。
三、setup函数
在vue2的时候,在单文件组件中,我们都是在data,watch,computed,methods等选项中编写我们的逻辑代码,虽然也很有效,但是他也带来了一些问题,当我们的组件变得很大,逻辑很复杂时,代码逻辑就会变得很难阅读和理解,特别是对于未参与编写的人来说,简直是灾难。
但是有了compositionApi之后,我们就可以把我们的变量声明和逻辑处理函数放在一起,这样的话阅读起来就方便多了,而使用conpositionApi的地方,就是setup函数!
新的setup函数在组件被创建之前执行,一旦props被解析完成,他就将被作为组合式API的入口;
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
// 业务代码
// ...
return {
// 需要给 template 用的数据、函数放这里 return 出去...
}
},
})
需要注意的是,在vue2中,我们是通过this来获取实例,拿到他的数据以及执行他的方法,但是在setup函数里面,你可千万不要这么做!
setup的参数使用
| 参数 | 类型 | 含义 | 是否必传 |
|---|---|---|---|
| props | Object | 父组件传递下来的数据 | 否 |
| context | Object | 组件执行上下文 | 否 |
props: 他是相应式的,前提是你不对它结构或者使用toRef或toRefs进行响应式数据转换,否则只要传入新的props,他都将更新。
context:它是一个非响应式的普通对象,暴露三个组件的property:
| 属性 | 类型 | 作用 |
|---|---|---|
| attrs | 非响应式对象 | props未包含的属性都将变成attrs |
| slots | 非响应式对象 | 插槽 |
| emits | 方法 | 触发事件 |
因为context是一个非响应式的普通对象,所以我们在使用的时候可以通过解构的方式单独引入某个属性,也可以通过context.xxx来使用,比如可以通过{emit}来引入,然后通过emit.xxx来使用他的属性。
但是对于attrs和slot,千万不要解构这两个的属性,坚持通过slot.xxx和attrs.xxx来使用他们的属性,因为虽然他们不是响应式的,但是他们也会随着组件本身的更新而更新。
四、响应式API之ref
在vue3中,ref将会是我们最常用的一个API,我们可以用它来声明所以类型的数据,同时也可以获取Node节点,就是我们vue2中的this.$ref.xxx。
变量类型声明
在声明一个变量的时候,我们都会给他一个类型声明,使用ref定义变量时类型声明的方法如下:
在ref中,类型声明都需要使用泛型,即使用<>包裹;
// 变量只有一个类型
const msg = ref<string>('Hello World!');
// 变量有多个类型
const city = ref<string | null>('深圳');
// 引用类型
const ids = ref<number[]>([1, 2, 3])
// 也可以自己声明一个类型
interface User {
name: string
age: number
sex?: string
}
const user = ref<User>({
name: 'Pitter',
age: 18,
sex: '男',
})
以上就是使用ref声明变量的方法了,除此之外,ref还有一个作用,就是获取DOM节点和子组件,下面介绍如何获取DOM元素和子组件。
<template>
<!-- 挂载DOM元素 -->
<p ref="msg">
给想要获取的节点加上ref属性
</p>
<!-- 挂载子组件 -->
<Child ref="child" />
给想要获取的子组件加上ref属性
<!-- 挂载子组件 -->
</template>
script部分
import { defineComponent, onMounted, ref } from 'vue'
import Child from './Child.vue'
export default defineComponent({
components: {
Child
},
setup () {
const msg = ref<HTMLElement | null>(null);
const child = ref<typeof Child | null>(null);
// 请保证视图渲染完毕后再执行节点操作 e.g. onMounted / nextTick
onMounted( () => {
// 比如获取DOM的文本
console.log(msg.value.innerText);
// 或者操作子组件里的数据
child.value.isShowDialog = true;
});
// 必须return出去才可以给到template使用
return {
msg,
child
}
}
})
注意点:
1、无论是使用ref定义的变量还是获取的DOM元素,都必须使用xxx.value才能正确获取到;
2、只能在视图渲染完成之后才能进行DOM或组件相关的操作,需要放到onMounted生命周期或者nextTick里进行操作;
3、变量必须return出去才能给template使用
另外,关于这一小节,有一个可能会引起 TS 编译报错的情况是,新版本的脚手架创建出来的项目会默认启用 --strictNullChecks 选项,会导致案例中的代码无法正常运行(报错 TS2531: Object is possibly 'null'. )。
原因是:默认情况下 null 和 undefined 是所有类型的子类型,但开启了 strictNullChecks 选项之后,会使 null 和 undefined 只能赋值给 void 和它们各自,这虽然是个更为严谨的选项,但因此也会带来一些影响赶工期的额外操作。
关于这个问题,有如下几种解决方案可供参考:
1、在对DOM节点或子组件进行操作时,增加一个判断
if ( child.value ) {
// 读取子组件的数据
console.log(child.value.num);
// 执行子组件的方法
child.value.sayHi('use if in onMounted');
}
2、通过 TS 的可选符?来将目标设置为可选,避免出现错误(这个方式不能直接修改子组件数据的值)
// 读取子组件的数据
console.log(child.value?.num);
// 执行子组件的方法
child.value?.sayHi('use ? in onMounted');
3、在项目根目录下的 tsconfig.json 文件里,显式的关闭 strictNullChecks 选项,关闭后,由自己来决定是否需要对 null 进行判断:
{
"compilerOptions": {
// ...
"strictNullChecks": false
},
// ...
}
4、使用any定义变量类型,但是能不要就不用。。
ref变量的使用
前面也提到了,使用ref定义的变量,必须使用xxx.value来使用
const msg = ref<string>('Hi!');
console.log(msg.value) // 'Hi!'
五、响应式API之reactive
reactive是除ref之外最常用的一个响应式API,但是它只能声明对象和数组;
类型声明及定义变量
与ref不同的是,它的类型声明是写在变量名后面的,且不需要<>包裹,就和普通变量一样;
// 使用reactive声明一个对象
// 定义类型
interface User {
name: string
age: number
sex?: string
}
// 定义对象
const user: User = reactive({
name: 'Pitter',
age: 18,
sex: '男',
})
// 使用reactive声明一个数组
const ids: number[] = reactive([1, 2, 3])
const member: User[] = reactive([
{
name: 'Tom',
age: 18,
sex: '男',
},
{
name: 'tony',
age: '21',
},
])
变量的使用
对于对象来说,使用reactive声明的对象和普通对象的使用方法是一样的
// 定义类型
interface User {
name: string
age: number
sex?: string
}
// 定义对象
const user: User = reactive({
name: 'Pitter',
age: 18,
sex: '男',
})
// 读取
console.log(user.name) // 'Pitter'
// 修改
user.name = 'Fully'
console.log(user.name) // 'Fully'
对于使用reactive声明的数组的使用就要注意了,因为如果操作不当的话,我们的数组变量是很容易失去响应性的,这样很容易导致数据更新了,但是你的template模板却没有更新;
对于普通数组,亦或者在vue2时,我们对数组的操作可能比较随意;
// 定义一个普通数组
let numbers: number[] = [ 1, 2, 3 ];
// 从另外一个对象数组里提取数据过来
numbers = data.map( item => item.id );
// 合并另外一个数组
let newNumbers: number[] = [ 4, 5, 6 ];
uids = [...uids, ...newUids];
// 重置数组
uids = [];
但是在对于reactive定义的数组,我们只能使用那些不会改变引用地址的操作,只有这样才不会失去响应性;
// 定义一个普通数组
let numbers: number[] = [ 1, 2, 3 ];
// 重置数组,此方法会失去响应性
// numbers = [];
// 重置数据,这个方法不会失去响应性
numbers.length = 0;
// 异步获取数据后,模板可以正确的展示
setTimeout( () => {
uids.push(1);
}, 1000);
同时,不要对reactive变量进行解构,不然同样也会失去响应性;
// 定义类型
interface User {
name: string
age: number
sex?: string
}
// 定义对象
const user: User = reactive({
name: 'Pitter',
age: 18,
sex: '男',
})
setTimeout(() => {
user.age = 28
},3000)
// 以下操作都会失去响应性
// 3s后age都不会更新
const { age } = user
const newUser = { ...user }
return {
...user,
}
六、响应式API之toRef与toRefs
到这里,我们应该对ref和reactive有了一定的了解了,其实他们两非常相似,作用也一样,但是都各有不方便之处,对于ref声明的变量,我们每次都必须通过xxx.value来使用它们,着实有点烦人,而使用reactive声明的变量,由于不能解构,使用在template中使用的时候,也必须使用类似user.xxx的方式,也是有点烦人,那有没有什么办法来解决这个问题呢?
当然有的,vue3给我们提供了两个用来转换ref和reactive的API,toRef和toRefs。接下来我们就来学习下这两个api的使用;
1、toRef: 创建一个新的变量,转换reactive的某一个属性为ref变量;
2、toRefs:创建一个新的对象,它的每个字段都是 reactive 对象各个字段的ref变量;
使用方法
toRef
它接受两个参数,第一个是reactive对象,第二个是要转换为ref变量的key;
// 定义类型
interface User {
name: string
age: number
sex?: string
}
// 定义对象
const user: User = reactive({
name: 'Pitter',
age: 18,
sex: '男',
})
const userName = toRef(user, 'name')
这样就将一个reactive对象的属性转换为了一个ref变量,接下来就可以通过.value的方式去对他进行操作了。
需要注意的是,如果我们转换的是一个reactive对象上不存在的key,那么这个ref变量的值就是undefined,当我们给这个ref变量赋值之后,原先的reactive上也会增加这个key属性和值
toRefs
只接收一个参数,即reactive对象
// 定义类型
interface User {
name: string
age: number
sex?: string
}
// 定义对象
const user: User = reactive({
name: 'Pitter',
age: 18,
sex: '男',
})
const newUser = toRefs(user)
这个新的对象newUser本身是一个普通对象,但是他的属性都是ref变量。
这样我们就完成了reactive向ref的转换,在使用的时候,我们在script中声明reactive对象,并进行操作,然后在最后return给template使用之前,使用toRefs进行转换,这样在模板中使用的时候就方便啦!
import { defineComponent, reactive, toRefs } from 'vue'
interface User {
name: string,
age: number,
sex: string
};
export default defineComponent({
setup () {
// 定义一个reactive对象
const userInfo = reactive({
name: 'Tom',
age: 18,
sex: '男'
})
// 定义一个新的对象,它本身不具备响应性,但是它的字段全部是ref变量
const userInfoRefs = toRefs(userInfo);
// 2s后更新userInfo
setTimeout( () => {
userInfo.name = 'Tony';
userInfo.age = 20;
}, 2000);
// 在这里解构toRefs对象才能继续保持响应式
return {
...userInfoRefs
}
}
})
需要注意的是,虽然我们可以return解构后的toRefs对象,但是一定要确保没有同名属性,否则将会以后面的为准,如下例子,return的数据中,userInfoRefs中含有name属性,然后又声明了一个name变量,将会以变量的name值作为模板的值
return {
...userInfoRefs,
name // 模板中的name的值以这个为准
}
七、总结
这一篇文章主要记录的是vue3中setup函数以及响应式API的使用,以及如何搭配TypeScript进行类型声明和一些注意事项,内容比较基础,在接触了vue3之后给我的感受就是,特别灵活方便,不仅给我们提供了两个响应式的API,同时还能进行转换,极大的满足了我们开发的需要,这里不得不给尤大大点个赞!同时也感觉自己还有学的东西很多,前端之路漫漫,加油!