一、Vue3介绍
2020年9月18日,Vue.js发布了3.0版本,代号:One Piece(海贼王)。
1.1. 升级
性能的提升
- 打包大小减少41%
- 初次渲染快55%,更新渲染快133%
- 内存减少54%
- ……
源码的升级
- 使用
Proxy
代替defineProperty
实现响应式 - 重写虚拟DOM的实现和
Tree-Shaking
(移除无效代码) - ……
- 使用
拥抱TypeScript
- Vue3可以更好的支持TypeScript
新的特性
- Composition API(组合API)
setup
配置ref
与reactive
watch
与watchEffect
provide
与inject
- 新的内置组件
Fragment
Teleport
Suspense
- 其他改变
- 新的生命周期钩子
data
选项应始终被声明为一个函数- 移除keyCode支持作为
v-on
的修饰符 - ……
- Composition API(组合API)
1.2. 创建
创建Vue3工程有两种方式:Vue CLI和Vite。
1.2.1. Vue-CLI
官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html
1 | 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上 |
1.2.2. Vite
官方文档:https://v3.cn.vuejs.org/guide/installation.html#vite
Vite(法语意为 “快速的”,发音 /vit/
,发音同 “veet”)是一种新型前端构建工具,能够显著提升前端开发体验。Vite官网:https://vitejs.cn。
优势:
- 开发环境中,无需打包操作,可快速的冷启动
- 轻量快速的热重载(HMR,Hot Mapper Reload)
- 真正的按需编译,不再等待整个应用编译完成
1 | # 创建工程 |
1.3. 入口
Vue3创建Vue实例的方式和Vue2不一样。而且在Vue3中,不能使用Vue2的方式创建实例。
1 | // main.js |
二、常用Composition API
官方文档:https://v3.cn.vuejs.org/guide/composition-api-introduction.html
2.1. setup
- setup是Vue3.0中一个新的配置项,值是一个函数。
- setup是所有Composition API(组合API)表演的舞台。
- 组件中所用到的数据、方法等均要配置在setup中。
- setup函数的两种返回值:
- 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。
- 若返回一个渲染函数,则可以自定义渲染内容。
- 注意点:
- 尽量不要与Vue2.x配置混用
- Vue2.x配置(data、methods、computed等)中可以访问到setup中的属性、方法。
- 在setup中不能访问到Vue2.x配置(data、methods、computed等)。
- 如果有重名,setup优先。
- setup不能是一个async函数,因为返回值不再是return的对象,而是promise,导致模板看不到return对象中的属性。如果setup是一个async函数,需要配合4.3中的异步引入组件实现。
- 尽量不要与Vue2.x配置混用
1 | <!-- 相比Vue2,在Vue3中template不需要根标签了 --> |
setup执行的时机在beforeCreate
之前执行一次,this
是undefined
。
setup的参数:
- props:值为对象,包含:组件外部传递过来,且组件内部声明接收了的属性。
- context:上下文对象
- attrs:值为对象,包含:组件外部传递过来,但没有在
props
配置中声明的属性,相当于Vue2中的this.$attrs
。 - slots:收到的插槽内容(
context.slots
),相当于Vue2中的this.$slots
。一定要注意在Vue3中,具名插槽使用<template></template>
进行包裹时,插槽名称使用v-slot:插槽名称
声明,否则可能在slots找不到插槽内容。 - emit:分发自定义事件的函数(
context.emit('add', '1')
),相当于Vue2中的this.$emit
。如果不添加emits
配置项(emits:['add']
),控制台会有警告。
- attrs:值为对象,包含:组件外部传递过来,但没有在
2.2. ref
作用:定义一个响应式的数据。
不能直接在
setup
的方法中直接修改定义的普通变量,否则及时数据改变了,但页面没有任何变化。需要借助ref
函数才可以触发Vue的响应式。在Vue2中
ref
可以用来查找标签,但是在Vue3中,ref
还是vue提供的一个函数,在setup
中定义/修改变量时使用ref
就可以使定义的变量成为响应式。语法:
const xxx = ref(***)
- 创建一个包含响应式数据的引用对象(reference实例对象,简称ref对象)
- JS中操作数据(setup):
xxx.value
- 模板中读取数据:不需要
.value
,直接使用即可:<h2>{{ xxx }}</h2>
数据类型
- 接收的数据可以是基本类型,也可以是对象类型
- 基本类型:响应式依然是靠
Object.defineProperty()
的get
与set
完成的。 - 对象类型:内部使用了Vue3中的一个新函数(
reactive
函数)。
1 | <template> |
2.3. reactive
作用:定义一个对象类型的响应式数据(基本类型不要用reactive,只能使用ref
函数)。
语法:const 代理对象 = reactive(被代理对象)
接收一个对象(或数组),返回一个代理器对象(Proxy
的实例对象,简称proxy对象)
reactive定义的响应式数据是“深层次”的,内部是基于ES6的Proxy实现,通过代理对象操作源对象内部数据都是响应式的。
在上面2.2中示例代码中,定义对象类型school
使用的是ref
,修改school
属性的时候需要先调用value
使其成为Proxy对象后才能修改属性值。如果定义school
使用reactive
,就不需要先调用value
了,因为生成的变量就是一个Proxy对象了,可以直接操作对象里面的属性。
1 | <template> |
2.4. reactive对比ref
- 从定义数据角度对比
- reactive用来定义:对象/数组类型数据。
- ref用来定义:基本类型数据,也可以用来定义对象/数组类型数据,它内部会自动通过reactive转为代理对象。
- 从原理角度对比
- ref通过
Object.defineProperty()
的get
和set
来实现响应式(数据劫持)。 - reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。
- ref通过
- 从使用角度对比
- ref定义的数据:操作数据需要
.value
,读取数据时模板中直接读取不需要.value
。 - reactive定义的数据:操作数据与读取数据都不需要
.value。
- ref定义的数据:操作数据需要
开发中使用reactive多一点,虽然reactive不能定义基本类型数据,但是大部分基本类型数据都在一个对象中,因此我们只需要对这个对象使用reactive就可以了。
2.5. 响应式原理
2.5.1. Vue2.x
对象类型:通过Object.defineProperty()
给对象的已有属性值的读取/修改进行拦截(数据劫持)。
数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)。
1 | // 模拟Vue2.x对象类型实现响应式 |
缺点:
- 新增属性、删除属性时,界面不会更新
- 直接通过下标修改数组,界面不会自动更新
2.5.2. Vue3.x
通过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等。
通过Reflect(反射):对源对象的属性进行操作。
MDN描述Proxy与Reflect:
- Proxy:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy
- Reflect:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect
之前我们使用Object.defineProperty()
可以进行对象的读写操作,但是如果报错了,需要我们主动使用try...catch
捕获错误。Reflect也有defineProperty
函数,使用方法和Object一样,不同的是Reflect.defineProperty()
有返回值,true
代表操作成功,false
代表操作失败。Reflect还可以进行对象的读写操作。
1 | // 模拟Vue3.x的Proxy实现响应式 |
2.6. computed
与Vue2.x中的computed配置功能一致。
1 | <template> |
2.7. watch
与Vue2.x中的watch配置功能一致。
2.7.1. 基本使用
两个小坑:
- 监视
reactive
定义的响应式数据时:oldValue
无法正常获取,强制开启了深度监视(deep
配置失效) - 监视
reactive
定义的响应式数据中某个属性时:deep
配置有效。
1 | let sum = ref(0) |
对象类型,newValue
和oldValue
会失效(值都一样)。
只有监听对象,deep
配置才生效。监听的属性是对象类型,要配置deep:true
,否则监听不到。
2.7.2. watchEffect
watch的套路:既要指明监视的属性,也要指明监视的回调。
watchEffect的套路:不用指明监视哪个属性,回调函数中用到哪个属性,那就监视哪个属性。
watchEffect有点像computed:
- computed:注重的是计算出来的值(回调函数的返回值),所以必须要写返回值
- watchEffect:注重的是过程(回调函数的函数体),所以不用写返回值
1 | // 回调函数中用到的数据只要发生变化,就会执行回调 |
2.8. 生命周期
Vue3的生命周期变化不大,在Vue3中可以继续使用Vue2中的生命周期钩子,但是有两个被更名:
beforeDestroy
修改为beforeUnmount
destroy
修改为unmounted
Vue3也提供了CompositionAPI形式的生命周期钩子,与Vue2中钩子对应关系:
1 | beforeCreate ===> setup() |
如果使用CompositionAPI实现了生命周期钩子,在配置项中也使用了生命周期钩子,执行顺序是:
1 | setup ==> |
2.9. hook
hook本质上是一个函数,把setup
函数中使用的CompositionAPI进行了封装。类似于Vue2中的mixin
。
自定义hook的优势:让setup
中的逻辑更清楚易懂,而且能够起到复用效果。
1 | <!-- 场景:鼠标打点 --> |
把逻辑放到hooks/usePoint.js
中:
1 | import {reactive, onMounted, onBeforeUnmount} from 'vue' |
这样就完成了鼠标打点功能,并且其他人在使用的时候,只需要把usePoint
导入调用即可。
hooks文件命名一般使用
use
前缀,例如:usePoint.js
。
2.10. toRef
作用:创建一个ref
对象,其value
值指向另一个对象中的某个属性值。
语法:const name = toRef(person, 'name')
。
场景:要将响应式对象中的某个属性单独提供给外部使用时。
扩展:toRefs
与toRef
功能一致,但可以批量创建多个ref
对象(一次性处理一个对象的所有属性),语法:toRefs(person)
。
1 | <template> |
上面的示例代码中,pName
就是一个普通变量(let pName = person.name
),所以修改person
的属性,pName
肯定不会改变。如果把person.name
加上toRef
(refName: toRef(person.name)
),refName
就会变成响应式,因为refName
的值就是RefImpl
类型(value
是person
的name
,Object
是person
)。
不要试图使用
ref
替换toRef
,即refName: ref(person.name)
,这样是无效的,相当于仅仅是把person.name
的值重新包装成了一个新的Ref对象,和person
没有任何关系。总结一句:
toRef
是引用,ref
是新建。
三、其他Composition API
3.1. shallowReactive 与 shallowRef
shallowReactive
:只处理对象最外层属性的响应式(浅响应式)。shallowRef
:只处理基本数据类型的响应式,不进行对象的响应式处理。
什么时候使用?
- 如果一个对象数据,结构比较深,但变化时只是外层属性变化,使用
shallowReactive
。 - 如果一个对象数据,后续功能不会修改该对象中的属性,而是生成新的对象替换,使用
shallowRef
。
3.2. readonly 与 shallowReadonly
readonly
:让一个响应式数据变为只读的(深层次只读)。shallowReadonly
:让一个响应式数据变为只读的(浅层次只读)。
应用场景:不希望数据被修改(对外暴露的数据不被修改,对内还是响应式)。
3.3. toRaw 与 markRaw
toRaw
- 作用:将一个由
reactive
生成的响应式对象转为普通对象(注意不能应用在ref
定义的变量上面)。 - 应用场景:用于读取响应式对象对应的普通对象,对这个普通对象的所有操作,不会引起页面更新(但数据会改变)。
- 作用:将一个由
markRaw
- 作用:标记一个对象,使其永远不会再成为一个响应式对象。
- 应用场景:有些值不应被设置为响应式的,例如复杂的第三方类库等。
1 | let person = reactive({ |
3.4. customRef
作用:创建一个自定义的ref
,并对其依赖项跟踪和更新触发进行显式控制。
下面示例代码实现防抖效果:
1 | <template> |
3.5. provide 与 inject
作用:实现祖孙组件间通信。
套路:祖组件有一个provide
选项来提供数据,后代组件有一个inject
选项来开始使用这些数据。
1 | // 祖组件 |
虽然官方定义的是跨级组件间通信,但是
provice
和inject
依然适用于父子组件间通信,只不过父子组件通信一般使用props
。
3.6. 响应式数据的判断
isRef
:检查一个值是否为一个ref
对象
isReactive
:检查一个对象是否由reactive
创建的响应式代理
isReadonly
:检查一个对象是否由readonly
创建的只读代理
isProxy
:检查一个对象是否由reactive
或readonly
创建的只读代理
3.7. Composition API的优势
Vue2中使用的是Options API,它存在的问题是新增或修改一个需求,就需要分别在data
,methods
,computed
里修改,功能代码具有离散性,维护成本增加。
在Composition API中,我们可以更加优雅的组织代码,函数。让相关功能的代码更加有序的组织在一起。
四、新的组件
4.1. Fragment
在Vue2中:组件必须有一个根标签。
在Vue3中:组件可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中。好处就是能够减少标签层级,减小内存占用。
4.2. Teleport
Teleport
是一种能够将我们的组件html结构移动到指定位置的技术。
1 | <!-- 子组件编写一个弹窗,弹窗显示在body上 --> |
4.3. Suspense
等待异步组件时渲染一些额外内容,让应用有更好的用户体验。
一般情况下,静态引入会导致等待所有组件加载完毕后再一起渲染到页面上(等待加载耗时最久的那个组件)。异步引入的组件不影响其他静态引入组件的渲染,等待加载完成后再去渲染。但是异步引入会有等待期,Suspense组件就可以让异步组件加载期间,先渲染其他已配置好的内容,加载完成后再把加载时渲染的组件替换为真正需要渲染的组件。
1 | <template> |
五、迁移
5.1. 全局API的转移
Vue2中有需要全局API和配置,例如:注册全局组件、注册全局指令等。
1 | // 注册全局组件 |
Vue3中对这些API做了调整:将全局的API(即Vue.xxx
)调整到应用实例(app
)上。
2.x全局API(Vue ) |
3.x实例API(app ) |
备注 |
---|---|---|
Vue.config.xxx | app.config.xxx | 配置项 |
Vue.config.productionTip | 移除 | 是否提示生产错误 |
Vue.component | app.component | 注册全局组件 |
Vue.directive | app.directive | 添加全局指令 |
Vue.mixin | app.mixin | 混入 |
Vue.use | app.use | 使用中间件 |
Vue.prototype | app.config.globalProperties | 操作原型 |
5.2. 其他改变
data
选项应始终被声明为一个函数。过渡类名的修改:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/* Vue2.x */
.v-enter, .v-enter-to {
opacity: 0;
}
.v-leave, .v-leave-to {
opacity: 1;
}
/* Vue3.x */
.v-enter-from, .v-leave-to {
opacity: 0;
}
.v-leave-from, .v-from-to {
opacity: 1;
}移除keyCode作为
v-on
的修饰符(@keyup.13=""
),同时也不再支持定义别名按键Vue.config.keycodes.huiche = 13
)移除
v-on.native
修饰符,新增emits
选项1
2
3
4
5
6
7
8
9
10
11
12
13
14
15<!-- Vue3.x -->
<template>
// 父组件中绑定事件
<my-component>
v-on:close="handleComponentEvent"
v-on:click="handleNativeClickEvent"
</>
</template>
// 子组件中声明自定义事件
<script>
export default {
emits: ['close']
}
</script>移除过滤器(
filter
)。官方:过滤器虽然看起来很方便,但它需要一个自定义方法,打破了大括号内的表达式只是JavaScript的假设,学习和实现都有成本,建议用方法或计算属性去替换过滤器。……