【Vue】系列五 - Vue3


一、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配置
      • refreactive
      • watchwatchEffect
      • provideinject
    • 新的内置组件
      • Fragment
      • Teleport
      • Suspense
    • 其他改变
      • 新的生命周期钩子
      • data选项应始终被声明为一个函数
      • 移除keyCode支持作为v-on的修饰符
      • ……

1.2. 创建

创建Vue3工程有两种方式:Vue CLIVite

1.2.1. Vue-CLI

官方文档:https://cli.vuejs.org/zh/guide/creating-a-project.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看@vue/cli版本,确保@vue/cli版本在4.5.0以上
vue --version

# 安装或升级@vue/cli
npm install -g @vue/cli

# 创建工程
vue create 工程名

# 进入到工程根目录
cd 工程名

# 启动工程
npm run serve
# OR
yarn serve

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
2
3
4
5
6
7
8
9
10
11
## 创建工程
npm init vite-app 工程名

## 进入工程目录
cd 工程名

## 安装依赖
npm install

## 运行
npm run dev

1.3. 入口

Vue3创建Vue实例的方式和Vue2不一样。而且在Vue3中,不能使用Vue2的方式创建实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// main.js
// 引入的不在是Vue2的构造函数函数了,引入的是一个名为createApp的工厂函数
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

// vue2的写法:
/*
import Vue from 'vue'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
render: h => h(App)
}).$mount('#app')
*/

二、常用Composition API

官方文档:https://v3.cn.vuejs.org/guide/composition-api-introduction.html

2.1. setup

  1. setup是Vue3.0中一个新的配置项,值是一个函数。
  2. setup是所有Composition API(组合API)表演的舞台
  3. 组件中所用到的数据、方法等均要配置在setup中。
  4. setup函数的两种返回值:
    • 若返回一个对象,则对象中的属性、方法,在模板中均可以直接使用。
    • 若返回一个渲染函数,则可以自定义渲染内容。
  5. 注意点:
    • 尽量不要与Vue2.x配置混用
      • Vue2.x配置(data、methods、computed等)中可以访问到setup中的属性、方法。
      • 在setup中不能访问到Vue2.x配置(data、methods、computed等)。
      • 如果有重名,setup优先。
    • setup不能是一个async函数,因为返回值不再是return的对象,而是promise,导致模板看不到return对象中的属性。如果setup是一个async函数,需要配合4.3中的异步引入组件实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
<!-- 相比Vue2,在Vue3中template不需要根标签了 -->
<template>
<h1>{{ title }}</h1>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<button @click="sayHello">说话(setup)</button>
<button @click="sayWelcome">说话(methods)</button>
<button @click="readNameFromSetupAtMethods">
methods中获取setup中的name
</button>
<button @click="readTitleFromMethodsAtSetup">
setup中获取methods中的title
</button>
</template>

<script>
import { h } from "vue";

export default {
name: "App",
data() {
return {
title: "daben",
};
},
methods: {
sayWelcome() {
console.log("欢迎使用", this.title); // 输出:欢迎使用 daben
},
readNameFromSetupAtMethods() {
console.log(this.name); // 输出:张三
},
},
setup() {
// 数据
let name = "张三";
let age = 18;
let title = "setup的标题";

// 方法
function sayHello() {
console.log(`姓名:${name},年龄:${age}`); // 输出:姓名:张三,年龄:18
}

function readTitleFromMethodsAtSetup() {
console.log(this.title, title); // 输出:undefined, setup的标题
// 注意:如果setup返回对象中有title,就会优先读取setup中的title,而且this.title有值
}

// 返回一个对象,一定要把外面需要使用到setup中的属性和函数名暴露出去,否则无法正常访问
return {
// title,
name,
age,
sayHello,
readTitleFromMethodsAtSetup,
};

// 返回一个渲染函数,需要导入vue中的渲染函数h
// return () => h('h1', '你好');
},
components: {},
};
</script>

<style></style>

setup执行的时机在beforeCreate之前执行一次,thisundefined

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']),控制台会有警告。

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()getset完成的。
    • 对象类型:内部使用了Vue3中的一个新函数(reactive函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
<!-- Vue3中如果插值类型是ref类型,会自动解析其value,所以不需要写成name.value -->
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<h2>学校:{{ school.name }}</h2>
<h2>毕业:{{ school.endTime }}</h2>
<button @click="changeInfo">修改信息</button>
</template>

<script>
import { ref } from "vue";

export default {
name: "App",
setup() {
/* 这种方式无法让数据成为响应式
let name = "张三";
let age = 18;

function changeInfo() {
name = '李四',
age = 30
}
*/

let name = ref("张三")
let age = ref(18)
let school = ref({
name: '南京理工大学',
endTime: '2015'
})

function changeInfo() {
name.value = "李四"
age.value = "30"
school.value.name = "东南大学"
school.value.endTime = "2020"
}

return {
name,
age,
school,
changeInfo
};
},
};
</script>

<style></style>

2.3. reactive

作用:定义一个对象类型的响应式数据(基本类型不要用reactive,只能使用ref函数)。

语法:const 代理对象 = reactive(被代理对象)接收一个对象(或数组),返回一个代理器对象(Proxy的实例对象,简称proxy对象)

reactive定义的响应式数据是“深层次”的,内部是基于ES6的Proxy实现,通过代理对象操作源对象内部数据都是响应式的。

在上面2.2中示例代码中,定义对象类型school使用的是ref,修改school属性的时候需要先调用value使其成为Proxy对象后才能修改属性值。如果定义school使用reactive,就不需要先调用value了,因为生成的变量就是一个Proxy对象了,可以直接操作对象里面的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<template>
<h2>姓名:{{ name }}</h2>
<h2>年龄:{{ age }}</h2>
<!-- 4. 读取对象属性值 -->
<h2>学校:{{ school.name }}</h2>
<h2>毕业:{{ school.endTime }}</h2>
<h2>英语:{{ school.tags.other.enLevel }}</h2>
<h2>
爱好:
<ul>
<li v-for="(hobbyItem, index) in school.tags.hobby" :key="index">
{{ hobbyItem }}
</li>
</ul>
</h2>
<div v-show="school.major">
<h2>专业:{{ school.major }}</h2>
<button @click="deleteMajor">删除属性</button>
</div>
<button @click="changeInfo">修改信息</button>
</template>

<script>
// 1. 引入reactive
import { ref, reactive } from "vue";

export default {
name: "App",
setup() {
let name = ref("张三");
let age = ref(18);
// 2. 使用reactive函数
let school = reactive({
name: "南京理工大学",
endTime: "2015",
tags: {
hobby: ["看电影"],
other: {
enLevel: "四级",
},
},
});

function changeInfo() {
name.value = "李四";
age.value = "30";
// 3. 直接修改
school.name = "东南大学";
school.endTime = "2020";
// 深层次修改
school.tags.other.enLevel = "六级";
// 修改数组
school.tags.hobby.push("睡觉");
// 通过索引修改,在Vue2.x是不行的,但是在Vue3.x是可以的
school.tags.hobby[0] = "娱乐";
// 对象新增属性
school.major = "软件工程";
}

function deleteMajor() {
// 对象删除属性
delete school.major;
}

return {
name,
age,
school,
changeInfo,
deleteMajor,
};
},
};
</script>

<style></style>

2.4. reactive对比ref

  • 从定义数据角度对比
    • reactive用来定义:对象/数组类型数据
    • ref用来定义:基本类型数据,也可以用来定义对象/数组类型数据,它内部会自动通过reactive转为代理对象。
  • 从原理角度对比
    • ref通过Object.defineProperty()getset来实现响应式(数据劫持)。
    • reactive通过使用Proxy来实现响应式(数据劫持),并通过Reflect操作源对象内部的数据。
  • 从使用角度对比
    • ref定义的数据:操作数据需要.value,读取数据时模板中直接读取不需要.value
    • reactive定义的数据:操作数据与读取数据都不需要.value。

开发中使用reactive多一点,虽然reactive不能定义基本类型数据,但是大部分基本类型数据都在一个对象中,因此我们只需要对这个对象使用reactive就可以了。

2.5. 响应式原理

2.5.1. Vue2.x

对象类型:通过Object.defineProperty()给对象的已有属性值的读取/修改进行拦截(数据劫持)。

数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 模拟Vue2.x对象类型实现响应式
let person = {
name: '张三',
age: 18
}

Object.defineProperty(person, 'name', {
configurable: true, // 可读写
get() {
return person.name
},
set(value) {
console.log("开始修改person的name属性")
person.name = value
console.log("已经修改person的name属性")
}
})

// 对象的增删改查
person.name // 输出:18
person.name = '李四' // 输出:开始修改person的name属性 已经修改person的name属性
delete person.name // 输出:false

缺点:

  • 新增属性、删除属性时,界面不会更新
  • 直接通过下标修改数组,界面不会自动更新

2.5.2. Vue3.x

通过Proxy(代理):拦截对象中任意属性的变化,包括:属性值的读写、属性的添加、属性的删除等。

通过Reflect(反射):对源对象的属性进行操作。

MDN描述Proxy与Reflect:

之前我们使用Object.defineProperty()可以进行对象的读写操作,但是如果报错了,需要我们主动使用try...catch捕获错误。Reflect也有defineProperty函数,使用方法和Object一样,不同的是Reflect.defineProperty()有返回值,true代表操作成功,false代表操作失败。Reflect还可以进行对象的读写操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 模拟Vue3.x的Proxy实现响应式
let person = {
name: '张三',
age: 18
}

let p = new Proxy(person, {
// 读取属性
// target:传入Proxy的对象
// propertyName:对象的属性名
get(target, propName) {
console.log(`读取了p的【${propName}】属性`)
// return target[propName]
return Reflect.get(target.propName)
},
// 修改或新增属性
// value:修改的值
set(target, propName, value) {
console.log(`修改了p的【${propName}】属性,可以去更新界面`)
// target[propName] = value
Reflect.set(tareget, propName, value)
},
// 删除属性
deleteProperty(target, propName) {
console.log(`删除了p的【${propName}】属性,可以去更新界面`)
// return delete target[propName]
return Reflect.deleteProperty(target, propName)
},
})

2.6. computed

与Vue2.x中的computed配置功能一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<template>
姓:<input type="text" v-model="person.firstName" />
<br>
名:<input type="text" v-model="person.lastName" />
<h2>姓名:{{ fullName }}</h2>
</template>

<script>
// 1. 引入computed
import { reactive, computed } from "vue";

export default {
name: "App",
setup() {
let person = reactive({
firstName: '张',
lastName: '三'
});
// 2. 使用计算属性computed
/* 简写(函数),只读
let fullName = computed(() => {
return person.firstName + person.lastName
})
*/

// 完整写法(对象),读写
// 假设有一个输入框,里面输入的姓名是使用’-’进行分割的
let fullName = computed({
get() {
return person.firstName + '-' + person.lastName
},
set(value) {
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})

// 建议把fullName放到person中,使用person.fullName获取值
// person.fullName = fullName

return {
person,
fullName
};
},
};
</script>

<style></style>

2.7. watch

与Vue2.x中的watch配置功能一致。

2.7.1. 基本使用

两个小坑:

  • 监视reactive定义的响应式数据时:oldValue无法正常获取,强制开启了深度监视(deep配置失效)
  • 监视reactive定义的响应式数据中某个属性时:deep配置有效。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
let sum = ref(0)
let msg = ref('你好')
let person = reactive({
name: '张三',
age: 18,
job: {
title: '开发'
}
})

// 情况一:监视ref定义的响应式数据
// 第三个参数是配置项
// 注意:不能监视sum.value,因为sum.value是一个具体的值,而不是变量
watch(sum, (newValue, oldvalue)) => {
console.log('sum变化了', newValue, oldValue)
},{immediate:true})

// 情况二:监视多个ref定义的响应式数据
// 注意:无法区分哪个属性变化,newValue和oldValue的值都在一个数组里面
watch([sum, msg], (newValue, oldvalue)) => {
console.log('sum或msg变化了', newValue, oldValue)
})

// 情况三:监视reactive定义的响应式数据
// 若watch监视的是reactive定义的响应式数据,则无法正确获得oldValue(即使用ref定义对象,监听person.value时,内部还是会自动转换为reactive(仅限对象类型))
// 若watch监视的是reactive定义的响应式数据,则强制开启了深度监视
// 注意:如果使用ref定义person,就不会监视到person属性值的变化,因为监听的是ref的value变化,而ref定义的对象数据person的value是Proxy对象,而Proxy对象包含person的一系列属性,修改person属性值,Proxy地址没有变,因此person的value也就没有改变,最终无法监视到person属性值变化。如果要实现ref定义对象的监视,有两种方式:1. 监视person.value,因为person.value是reactive生成的Proxy数据,是自动强制开启深层监视的。2. 开启深度监视(deep:true)
watch(person, (newValue, oldvalue)) => {
console.log('person变化了', newValue, oldValue)
},{immediate:true, deep:false}) // 此处的deep:false配置不在生效

// 情况四:监视reactive定义的响应式数据中的某个属性
// 监视属性必须写成一个函数,监视哪个属性函数返回哪个属性
watch(() => person.name, (newValue, oldvalue)) => {
console.log('person的name变化了', newValue, oldValue)
},{immediate:true, deep:true}) // 此处的deep:true配置不在生效

// 情况五:监视reactive定义的响应式数据中的多个属性
watch([() => person.name, () => person.age], (newValue, oldvalue)) => {
console.log('person的name变化了', newValue, oldValue)
})

// 特殊情况:监听reactive定义的响应式数据中的深层次属性
// 该属性一定是一个对象才能正常监听,且newValue和oldValue依然失效
watch(() => person.job, (newValue, oldvalue)) => {
console.log('person的name变化了', newValue, oldValue)
}, {deep:true}) // 一定要写deep:true,否则不生效

对象类型,newValueoldValue会失效(值都一样)。

只有监听对象,deep配置才生效。监听的属性是对象类型,要配置deep:true,否则监听不到。

2.7.2. watchEffect

watch的套路:既要指明监视的属性,也要指明监视的回调。

watchEffect的套路:不用指明监视哪个属性,回调函数中用到哪个属性,那就监视哪个属性。

watchEffect有点像computed:

  • computed:注重的是计算出来的值(回调函数的返回值),所以必须要写返回值
  • watchEffect:注重的是过程(回调函数的函数体),所以不用写返回值
1
2
3
4
5
6
// 回调函数中用到的数据只要发生变化,就会执行回调
// 如下函数体重使用了sum和person.age,只要sum和person.age发生变化就会执行回调函数
watchEffect(() => {
const x1 = sum.value
const x2 = person.age
})

2.8. 生命周期

Vue3的生命周期变化不大,在Vue3中可以继续使用Vue2中的生命周期钩子,但是有两个被更名:

  • beforeDestroy修改为beforeUnmount
  • destroy修改为unmounted

Vue3也提供了CompositionAPI形式的生命周期钩子,与Vue2中钩子对应关系:

1
2
3
4
5
6
7
8
9
beforeCreate 	===> setup()
created ===> setup()
// 加上前缀on即可
beforeMount ===> onBeforeMount
mounted ===> onMonted
beforeUpdate ===> onBeforeUpdate
updated ===> onUpdated
beforeUnmount ===> onBeforeUnmount
unmounted ===> onUnmounted

如果使用CompositionAPI实现了生命周期钩子,在配置项中也使用了生命周期钩子,执行顺序是:

1
2
3
4
5
6
7
8
setup ==> 
beforeCreate ==> created ==>
// CompositionAPI的优先级更高
onBeforeMount ==> beforeMount ==>
onMounted ==> mounted ==>
onBeforeUpdate ==> beforeUpdate ==> onUpdated ==> updated ==>
onBeforeUnmount ==> beforeUnmount ==>
onUnmounted ==> unmounted

2.9. hook

hook本质上是一个函数,把setup函数中使用的CompositionAPI进行了封装。类似于Vue2中的mixin

自定义hook的优势:让setup中的逻辑更清楚易懂,而且能够起到复用效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 场景:鼠标打点 -->
<!-- Demo.vue -->
<template>
<h2>鼠标点击:{{point.x}}, {{point.y}}</h2>
</template>

<script>
import usePoint from '../hooks/usePoint'

export default {
name: "Demo",
setup() {
let point = usePoint();

return {
point
}
},
};
</script>

<style></style>

<!-- hooks/usePoint.js -->

把逻辑放到hooks/usePoint.js中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {reactive, onMounted, onBeforeUnmount} from 'vue'

export default function() {
let point = reactive({
x: 0,
y: 0
})

function savePoint(event) {
point.x = event.pageX
point.y = event.pageY
console.log(event);
}

onMounted(() => {
window.addEventListener('click', savePoint)
})

onBeforeUnmount(() => {
window.removeEventListener('click', savePoint)
})

return point
}

这样就完成了鼠标打点功能,并且其他人在使用的时候,只需要把usePoint导入调用即可。

hooks文件命名一般使用use前缀,例如:usePoint.js

2.10. toRef

作用:创建一个ref对象,其value值指向另一个对象中的某个属性值。

语法:const name = toRef(person, 'name')

场景:要将响应式对象中的某个属性单独提供给外部使用时。

扩展:toRefstoRef功能一致,但可以批量创建多个ref对象(一次性处理一个对象的所有属性),语法:toRefs(person)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
<template>
<h2>person.name:{{ person.name }}</h2>
<h2>pName:{{ pName }}</h2>
<h2>refName:{{ refName }}</h2>
<h2>pJobTitle:{{ pJobTitle }}</h2>
<h2>refJobTitle:{{ refJobTitle }}</h2>
<button @click="updateInfo">修改信息</button>
</template>

<script>
import { reactive, toRef } from "vue";

export default {
name: "Demo",
setup() {
let person = reactive({
name: "张三",
age: 18,
job: {
title: "前端",
},
});

function updateInfo() {
person.name = "李四";
person.job.title = "移动端";
}

return {
person,
pName: person.name,
refName: toRef(person, "name"),
pJobTitle: person.job.title,
refJobTitle: toRef(person.job, "title"),
updateInfo,
};

// 使用toRefs
/*
setup返回值:
return {
...toRefs(person)
}

页面使用:
<h2>{{ name }}</h2>
<h2>{{ job.title }}</h2>
*/
},
};
</script>

<style></style>

<!--
updateInfo执行前:
person.name:张三
pName:张三
refName:张三
pJobTitle:前端
refJobTitle:前端

updateInfo执行后:
person.name:李四
pName:张三
refName:李四
pJobTitle:前端
refJobTitle:移动端
-->

上面的示例代码中,pName就是一个普通变量(let pName = person.name),所以修改person的属性,pName肯定不会改变。如果把person.name加上toRefrefName: toRef(person.name)),refName就会变成响应式,因为refName的值就是RefImpl类型(valuepersonnameObjectperson)。

不要试图使用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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let person = reactive({
name: '张三',
age: 18
})

function showRawPerson() {
// 响应式person转为普通person
const p = toRaw(person)
}

function addHobby() {
const hobby = {
title: '看电影',
type: '1'
}
// hobby不会成为响应式
person.hobby = markRaw(hobby)
}

3.4. customRef

作用:创建一个自定义的ref,并对其依赖项跟踪和更新触发进行显式控制。

下面示例代码实现防抖效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
输入:<input type="text" v-model="keyword" />
<br />
显示:<span>{{ keyword }}</span>
</template>

<script>
import { ref, customRef } from "vue";

export default {
name: "Demo",
setup() {
// 使用官方的ref
// let keyword = ref('')

// 自定义ref(借助customRef)
function myRef(value, delay = 100) {
const x = customRef((track, trigger) => {
let timer;
return {
get() {
// 追踪value值的变化
track()
return value
},
set(newValue) {
// 通知Vue去重新解析模板(即重新调用上面的get函数获取最新value)
if (timer) {
timer = clearTimeout(timer)
}
timer = setTimeout(() => {
value = newValue
trigger()
}, delay);
},
};
});
return x;
}
let keyword = myRef("", 1000);

return {
keyword,
};
},
};
</script>

<style></style>

3.5. provide 与 inject

作用:实现祖孙组件间通信。

套路:祖组件有一个provide选项来提供数据,后代组件有一个inject选项来开始使用这些数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 祖组件
setup() {
......
let person = reactive({
name: '张三',
age: 18
})
provide('person', person)
}

// 后代组件
setup(props, context) {
......
const person = inject('person')
return {
person
}
}

虽然官方定义的是跨级组件间通信,但是proviceinject依然适用于父子组件间通信,只不过父子组件通信一般使用props

3.6. 响应式数据的判断

isRef:检查一个值是否为一个ref对象

isReactive:检查一个对象是否由reactive创建的响应式代理

isReadonly:检查一个对象是否由readonly创建的只读代理

isProxy:检查一个对象是否由reactivereadonly创建的只读代理

3.7. Composition API的优势

Vue2中使用的是Options API,它存在的问题是新增或修改一个需求,就需要分别在datamethodscomputed里修改,功能代码具有离散性,维护成本增加。

在Composition API中,我们可以更加优雅的组织代码,函数。让相关功能的代码更加有序的组织在一起。

四、新的组件

4.1. Fragment

在Vue2中:组件必须有一个根标签。

在Vue3中:组件可以没有根标签,内部会将多个标签包含在一个Fragment虚拟元素中。好处就是能够减少标签层级,减小内存占用。

4.2. Teleport

Teleport是一种能够将我们的组件html结构移动到指定位置的技术。

1
2
3
4
5
6
7
8
9
<!-- 子组件编写一个弹窗,弹窗显示在body上 -->
<teleport to="body">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>

4.3. Suspense

等待异步组件时渲染一些额外内容,让应用有更好的用户体验。

一般情况下,静态引入会导致等待所有组件加载完毕后再一起渲染到页面上(等待加载耗时最久的那个组件)。异步引入的组件不影响其他静态引入组件的渲染,等待加载完成后再去渲染。但是异步引入会有等待期,Suspense组件就可以让异步组件加载期间,先渲染其他已配置好的内容,加载完成后再把加载时渲染的组件替换为真正需要渲染的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<template>
<div class="app">
<h3>我是App组件</h3>
<!-- 2. 使用Suspense包裹组件,并配置好default和fallback -->
<Suspense>
<template v-slot:default>
<Child/>
</template>
<template v-slot:fallback>
<h3>加载中...</h3>
</template>
</Suspense>
</div>
</template>

<script>
import {defineAsyncComponent} from 'vue'
// 1. 异步引入组件
const Child = defineAsyncComponent(() => import('./components/Child'))
// 静态引入组件
// import Child from './components/Child'

export default {
name: 'App',
components: {Child}
}
</script>

五、迁移

5.1. 全局API的转移

Vue2中有需要全局API和配置,例如:注册全局组件、注册全局指令等。

1
2
3
4
5
6
7
8
9
10
11
12
// 注册全局组件
Vue.component('MyButton', {
data: () => {
count: 0
},
template: '<button @click="count++">点击 {{ count }}</button>'
})

// 注册全局指令
Vue.directive('focus', {
inserted: el => el.focus()
})

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的假设,学习和实现都有成本,建议用方法或计算属性去替换过滤器。

  • ……