一、前言
自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,同时还能进行转换,极大的满足了我们开发的需要,这里不得不给尤大大点个赞!同时也感觉自己还有学的东西很多,前端之路漫漫,加油!