1 简介
2020年9月18日,Vue.js发布3.0版本Vue3官网 Vue v3.0.0 One Piece。Vue3向下兼容Vue2.x版本,并且比Vue2的性能显著提升(打包大小减少41%,初次渲染速度提高55%,更新渲染提高133%,内存使用率减少54%)。Vue3使用Proxy代替defineProperty实现响应式,重写虚拟DOM的实现和Tree-Shaking,更友好兼容TypeScript。
2 常用Composition API
2.1 setup函数
组件中所用到的数据、方法、计算属性、监视属性、生命周期钩子等等都配置在setup中。
setup函数有两种返回值:
- 若返回一个对象,则对象中的属性方法在template模板中都可以直接使用(常用)
- 若返回一个渲染函数,则可以自定义渲染内容
<template>
<!-- setup函数返回对象的属性方法在模板中可以直接使用 -->
<h1>姓名:{{name}}</h1>
<button @click="sayHello"></button>
</template>
<script>
// import {h} from 'vue'
export default {
setup(){
// 数据
let name = '张三'
let age = 18
// 方法
function sayHello(){
alert(`我叫${name},我今年${age}岁`)
}
// 返回一个对象
return {
name,
age,
sayHello
}
// 返回一个渲染函数
// return ()=>{ return h('h1','你好') }
}
}
</script>
注意:
- 当Vue3与Vue2.x混用时(不建议),Vue2.x配置(data、methods、computed…)中可以访问到setup中的属性、方法,但在setup中不能访问到Vue2.x配置(data、methods、computed…)。如果有重名,setup优先。
- setup不能是一个async函数,因为setup被async修饰后,返回值不再是简单的对象,而是被promise包裹的对象,模板中看不到对象中的属性。(Suspense和异步组件配合时可以返回一个Promise实例)
- setup在
beforeCreate(){}
配置项之前执行(组合式API形式的beforeCreate在setup里面),this是undefined。 setup(props, context){}
接收到两个参数:
第一个是对象,包含组件外部传递进来且组件内部props: []
配置中声明接收了的属性;
第二个是context上下文对象,包括:- attrs:组件外部传递进来,但没有在props配置中声明的属性
- slots:收到的插槽内容
- emit:自定义事件的函数
2.2 ref函数
ref可以定义一个响应式数据,通过语法const xxx = ref(initValue)
创建一个引用对象RefImpl { …, value: initValue },对象中包含响应式数据,在JS中通过xxx.value
操作数据,在模板中读取数据时不需要.value,直接<div>{{xxx}}</div>
。
ref接收的数据可以是基本类型,也可以是对象类型。如果是基本类型的数据,响应式实现原理与Vue2一样,通过Object.defineProperty()
的get
和set
实现;如果是对象类型的数据,对象内部通过Vue3中的一个新函数reactive实现(本质上还是通过ES6的Proxy实现,Vue3把Proxy操作封装在reactive函数中)。
<template>
<h1>姓名:{{name}}</h1>
<h1>年龄:{{age}}</h1>
<h1>工作种类:{{job.type}}</h1>
<h1>工作薪水:{{job.salary}}</h1>
<button @click="changeInfo"></button>
</template>
<script>
import {ref} from 'vue'
export default {
setup(){
// name不再是字符串,是一个引用对象RefImpl{...,value:"张三"},对象中有一个属性值为张三
let name = ref('张三')
// age是普通数据,不是响应式数据
let age = 18
// ref函数处理对象类型
// job是ref引用对象RefImpl{...,value:Proxy{...}}
// job.value是Proxy{type:'前端工程师', salary:'30K'}
let job = ref({
type:'前端工程师',
salary:'30K'
})
function changeInfo(){
name.value = '李四' // 响应式数据,页面会更新
age = 20 // 不是响应式数据,数据被修改了但页面不会更新
job.value.type = 'UI设计师' // 响应式数据,页面会更新
}
return {
name,
age,
job,
changeInfo
}
}
}
</script>
2.3 reactive函数
reactive可以定义一个对象类型的响应式数据(基本类型要用ref函数),通过语法const 代理对象 = reactive(源对象)
接收一个对象或数组,返回一个代理对象,代理对象本质上是Proxy的实例对象,因为reactive内部是基于ES6的Proxy实现的,通过代理对象操作源对象内部数据。
reactive定义的响应式数据是深层次的。
<template>
<h1>姓名:{{person.name}}</h1>
<h1>年龄:{{person.age}}</h1>
<h1>工作种类:{{person.job.type}}</h1>
<h1>工作薪水:{{person.job.salary.max}}</h1>
<h1>爱好:{{person.hobby}}</h1>
<button @click="changeInfo"></button>
</template>
<script>
import {reactive} from 'vue'
export default {
setup(){
// reactive接收一个数组
// let hobby = reactive(['吃饭', '睡觉', '打豆豆'])
// reactive接收一个对象,person是Proxy{name: '张三', ...}
let person = reactive({
name: '张三',
age: 18,
job: {
type: '前端工程师',
salary: {
max: '15K',
min: '30K'
}
},
hobby: ['吃饭', '睡觉', '打豆豆'],
})
function changeInfo(){
person.name = '李四'
person.age = 20
person.job.type = 'UI设计师'
}
return {
person,
changeInfo
}
}
}
</script>
2.4 Vue3的响应式原理
2.4.1 Vue2.x的响应式
Vue2.x实现响应式,如果是对象类型,通过Object.defineProperty()
对属性的读取、修改进行拦截(数据劫持);如果是数组类型,通过重写更新数组的一系列方法来实现拦截,比如在vue中调用数组的push方法,其实调用的是vue经过二次封装的push,不是Array原型对象上的push,vue帮你先调用Array原型对象上的push方法,再更新界面。
// 对象类型通过Object.defineProperty()实现响应式
Object.defineProperty(对象, 属性名, {
get(){
return 对象.属性名
},
set(value){
对象.属性名 = value
}
})
Vue2.x实现响应式存在一些问题,新增属性、删除属性和直接通过下标修改数组,界面都不会自动更新,但这还是有解决的办法:
- 增加属性:
this.$set(对象, 属性名, 属性值)
或Vue.set(对象, 属性名, 属性值)
- 删除属性:
this.$delete(对象, 属性名)
或Vue.delete(对象, 属性名)
- 更新数组:
this.$set(数组, 下标, 值)
或数组.splice()
2.4.2 Vue3的响应式
在介绍Vue3响应式原理前先来介绍Reflect反射对象——window身上的一个内置对象。
Reflect.get(对象, 属性名)
、Reflect.set(对象, 属性名, value)
、Reflect.deleteProperty(对象, 属性名)
可以实现同对象.属性名
、对象.属性名 = value
、delete 对象.属性名
一样的效果,读取、修改和删除对象上的某一个属性。但是Object.defineProperty()
出错时,后续代码会停止运行,需要try catch抛出异常后才能跑下去,而Reflect即使出错了后续代码也能执行下去,并且会返回一个执行成功或失败的布尔值,可以通过判断布尔值真假指定操作。
let obj = {a:1, b:2}
Object.defineProperty(obj, 'c', {
get(){
return 3
},
set(value){}
})
Object.defineProperty(obj, 'c', {
get(){
return 4
},
set(value){}
})
// 此时会报错Cannot redefine property: c,不能重复定义属性c,需要try{}catch(error){}抛出异常,后面代码才能执行
const x1 = Reflect.defineProperty(obj, 'c', { // x1为true
get(){
return 3
},
set(value){}
})
const x2 = Reflect.defineProperty(obj, 'c', { // x2为false
get(){
return 4
},
set(value){}
})
if(x2){
// 可以指定x2为真时的操作
}
// 此时x2虽然执行失败,但后续代码还能跑下去,并且可以指定x1、x2执行成功或失败的操作
Vue3实现响应式首先通过Proxy代理拦截对象中任意属性的变化,包括属性的读写、添加和删除,再通过Reflect对源对象的属性进行操作。
const p = new Proxy(person,{ // person是原对象,p是代理对象
// 拦截,读取某个属性时调用
get(target,propName){
// target是new Proxy时传入的源对象,也就是person
// propName是某个属性
return Reflect.get(target, propName) // Reflect对源对象的属性进行读取
},
// 拦截,修改或增加某个属性时调用
set(target,propName){
Reflect.set(target, propName, value) // Reflect对源对象的属性进行修改或添加
},
// 拦截,删除某个属性时调用
deleteProperty(target,propName){
return Reflect.deleteProperty(target, propName) // Reflect对源对象的属性进行删除
},
})
2.4.3 ref与reactive对比
ref | reactive | |
---|---|---|
定义数据类型 | 基本类型 | |
(也可以定义对象或数组,内部通过reactive实现) | 对象或数组 | |
实现原理 | Object.defineProperty()的get和set | Proxy实现响应式,Reflect操作源对象的数据 |
使用方式 | JS中通过xxx.value 操作,模板中读取时不需要.value | 都不需要.value |
2.5 computed
import { computed } from 'vue'
export default {
setup() {
let person = {
firstName: '张',
lastName: '三',
}
// 计算属性简写(只读)
let name = computed(() => {
return person.firstName + '-' + person.lastName
})
// 计算属性完整写法(可修改)
let name = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
},
})
return {
person
}
}
}
2.6 watch
import {ref, watch} from 'vue'
export default {
// Vue2写法
watch: {
// 简写
sum(newValue, oldValue) {
...
},
// 完整写法
sum: {
immediate: true, // 立即监听
deep: true, // 深度监视
// 监视的回调
handler(newValue, oldValue){
...
}
}
},
setup() {
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
firstName: '张',
lastName: '三',
a:{
b:{
c: 1,
}
}
})
// Vue3写法
// 监视ref定义的一个响应式数据
watch(sum, (newValue, oldValue)=>{
...
}, {immediate: true})
// 监视ref定义的多个响应式数据,Vue2中只能写一个watch配置项,Vue3中可以调用多次watch函数
watch(sum, (newValue, oldValue)=>{
...
})
watch(msg, (newValue, oldValue)=>{
...
})
// 监视ref定义的多个响应式数据,简便写法
watch([sum, msg], (newValue, oldValue)=>{
// newValue、oldValue是数组,相当于[sum的newValue, msg的newValue]、[sum的oldValue, msg的oldValue]
...
})
// 监视reactive定义的对象
watch(person, (newValue, oldValue)=>{
// 无法获取正确的oldValue
// 监视的是reactive定义的对象,deep配置无效,强制开启深度监视
})
// 监视reactive定义的对象中的某个属性
watch(()=>person.firstName, (newValue, oldValue)=>{
// 可以获取正确的oldValue
})
// 监视reactive定义的对象中的某些属性
watch([()=>person.firstName, ()=>person.lastName], (newValue, oldValue)=>{
...
})
// 监视reactive定义的对象中的某个属性,这个属性也是对象
watch(()=>person.a, (newValue, oldValue)=>{
// 监视的是reactive定义的对象中的某个属性,所以deep配置有效
// 无法获取oldValue
}, {deep: true})
return {
sum,
msg,
person
}
}
}
监视ref定义的基本数据类型时,不需要.value
,.value
后相当于监视0这个数字本身。
监视ref定义的对象类型时,需要.value
,此时相当于监视reactive定义的对象。或者不加.value
,加deep: true
配置,此时可以监视到RefImpl对象中的属性。
2.7 watchEffect函数
watch函数既要指明监视的是哪个属性,也要指明监视的回调。watchEffect函数不用指明监视的是哪个属性,监视的回调中用到哪个属性就监视哪个属性。
// watchEffect所指定的回调中,用到的数据只要发生变化,就直接重新执行回调
watchEffect(()=>{
const x1 = sum.value
const x2 = person.age
})
2.8 Vue3的生命周期
Vue3中可以继续使用Vue2.x中的生命周期钩子,但有两个被更名:beforeDestroy
改为beforeUnmount
,destroyed
改为unmounted
。
// 通过配置项的形式使用生命周期钩子
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeUnmount() {},
unmounted() {},
}
Vue3也提供了Composition API形式的生命周期钩子,与Vue2.x中钩子对应关系如下:
beforeCreate
–>setup()
created
–>setup()
beforeMount
–>onBeforeMount
mounted
–>onMounted
beforeUpdate
–>onBeforeUpdate
updated
–>onUpdated
beforeUnmount
–>onBeforeUnmount
unmounted
–>onUnmounted
如果代码中同时写了配置项形式和Composition API形式的生命周期钩子,Composition API形式的优先,执行顺序为:setup–>beforeCreate–>created–>onBeforeMount–>beforeMount–>onMounted–>mounted。
// 通过组合式API的形式使用生命周期钩子
import {onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted} from 'vue'
export default {
setup() {
onBeforeMount(()=>{})
onMounted(()=>{})
onBeforeUpdate(()=>{})
onUpdated(()=>{})
onBeforeUnmount(()=>{})
onUnmounted(()=>{})
}
}
2.9 自定义hook函数
hook本质是一个函数,把setup函数中使用的Composition API进行封装,起到复用代码的作用。
2.10 toRef
要将响应式对象a中的某个属性b单独提供给外部使用时,可以通过toRef(a, 'b')
创建一个ref对象,其value值指向a中的属性b。toRefs与toRef功能一致,但可以批量创建多个ref对象。
import { reactive } from 'vue'
export default {
setup() {
let person = reactive({
name: '张三',
age: 18,
a: {
b: {
c: 1,
}
}
})
// 相当于name: '张三',返回出去的name、age不是响应式的
// return {
// name: person.name,
// age: person.age,
// }
// name是响应式,但是重新复制一份,修改name,person中的name属性不会改变
// return {
// name: ref(person.name),
// age: ref(person.age)
// }
// name是ref对象ObjectRefImpl{..., value: '张三', ...},属性value指向person中的name
return {
name: toRef(person, 'name'),
age: toRef(person, 'age'),
c: toRef(person.a.b, 'c')
}
// toRefs(person)是一个对象
// {name: ObjectRefImpl{..., value: '张三', ...}, age: ObjectRefImpl{}, job: {}}
return {
...toRefs(person)
}
}
}
3 其他Composition API
3.1 shallowReactive与shallowRef
shallowReactive的响应式是浅层次,只处理对象第一层属性的响应式。
let person = shallowReactive({
name: '张三', // 响应式
age: 18, // 响应式
a: {
b: {
c: 1, // 不是响应式
}
}
})
ref可以定义对象类型数据,内部通过调用reactive来处理,而shallowRef不能处理对象类型的响应式,只能定义基本类型的响应式数据。
// x是RefImpl{...,value:Proxy{y: 0}}
let x = ref({
y: 0
})
// x是RefImpl{...,value:{y: 0}}
let x = ref({
y: 0
})
3.2 readonly与shallowReadonly
person = readonly(person)
让一个响应式数据变为只读的,shallowReadonly浅只读。
3.3 toRaw与markRaw
toRaw将一个由reactive生成的响应式对象转为普通对象。
markRaw标记一个对象,使其永远不会再成为响应式对象。
3.4 customRef
3.5 provide与inject
作用:实现祖孙组件间通信
父组件有一个provide选项来提供数据,孙组件有一个inject选项来使用这些数据。(子组件也可以,但父子组件传递数据一般用props)
setup(){
...
let person = reactive({
name: '张三',
age: 18
})
provide('person', person)
...
}
setup(){
...
const person = inject('person')
return { person }
...
}
3.6 响应式数据的判断
isRef
检查一个值是否为一个ref对象isReactive
检查一个对象是否是由reactive创建的响应式代理isReadonly
检查一个对象是否是由readonly创建的只读代理isProxy
检查一个对象是否是由reactive或者readonly方法创建的代理
4 新的组件
4.1 Fragment
在Vue2.x中组件必须有一个根标签,在Vue3中组件可以没有根标签,内部会将多个标签包含在一个Fragment组件中。
4.2 Teleport
Teleport能够将组件结构移动到指定位置。
<teleport to="body">
<!-- 把dialog组件移动到body下 -->
<!-- <body>
...
<div class="dialog"></div>
...
</body> -->
<div class="dialog"></div>
</teleport>
4.3 Suspense
使用import xxx from './xxx/xxx'
引入子组件的方式是静态引入,这种引入方式子组件与父组件同时出现,如果子组件引入失败,那么父组件也不会进行渲染。
使用import {defineAsyncComponent} from 'vue'``const Child = defineAsyncComponent(()=>import('./xxx/xxx'))
引入子组件的方式是异步引入,父组件先出现,子组件后出现。如果等待子组件渲染的时间过长,或子组件突然出现,用户体验感差。
Suspense可以在等待异步组件时先渲染一些额外内容,如提示“加载中。。。”,让用户有更好的体验。
使用步骤:
- 异步引入组件
- 使用Suspense包裹组件,并配置好default和fallback
<template>
<div>
<h3>我是App父组件</h3>
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中。。。</h3>
</template>
</Suspense>
</div>
</template>
<script>
import {defineAsyncComponent} from 'vue'
const Child = defineAsyncComponent(()=>import('./xxx/xxx')) // 异步引入
export default {
name: 'App',
components: {
Child,
},
}
</script>