Vue3

Vue3

官网传送门

Vue3 的变化

性能的提升

  • 打包大小减少 41%
  • 初次渲染快 55%, 更新渲染快 133%
  • 内存减少 54%

源码的升级

  • 使用 Proxy 代替 defineProperty 实现响应式
  • 重写虚拟 DOM 的实现和 Tree-Shaking

拥抱 TypeScript

  • Vue3 可以更好的支持 TypeScript

新的特性

  1. Composition API(组合 API)
  • setup 配置
  • ref 与 reactive
  • watch 与 watchEffect
  • provide 与 inject
  • ……
  1. 新的内置组件
  • Fragment
  • Teleport
  • Suspense
  1. 其他改变
  • 新的生命周期钩子
  • data 选项应始终被声明为一个函数
  • 移除 keyCode 支持作为 v-on 的修饰符
  • ……

创建 Vue3 工程

vite 和 vue-cli 对比

vite vue-cli
支持的 vue 版本 仅支持 vue3.x 支持 2.x 和 3.x
是否基于 webpack
运行速度 较慢
功能完整度 小而巧 大而全
是否建议企业级开发使用 暂不建议 建议

使用 vue-cli 创建

1
2
3
4
5
6
7
8
9
### 查看 @vue/cli 版本,确保 @vue/cli 版本在4.5.0以上
vue --version
### 安装或者升级 @vue/cli
npm install -g @vue/cli
### 创建
vue create vue_test
### 启动
cd vue_test
npm run serve

使用 vite 创建

  • vite:新一代前端构建工具
  • 优势:
    • 开发环境中,无需打包操作,可快速冷启动(webpack 每次运行项目都要打包)
    • 轻量快速的热重载 HMR(更改代码局部刷新,webpack 也行,但 vite 更轻量)
    • 真正的按需编译,无需等待整个应用编译完成
  • 传统构建 与 vite 构建对比(vite 现用现分析,按需导入,因此项目启动更快)

Bundle

ESM

1
2
3
4
5
npm init vite-app 项目名称

cd 项目名称
npm install
npm run dev

Vue3 项目结构

Vue3 中 main.js 代码有所改变:

1
2
3
4
5
6
7
8
// 不再引入 Vue 构造函数,而是引入 createApp 工厂函数
// createApp函数:创建 vue 的 SPA 实例
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例对象
const app = createApp(App)
app.mount('#app')

Vue3 支持定义多个根节点,组件的 <template> 支持定义多个根节点:

1
2
3
4
<template>
<h1>根节点</h1>
<h1>根节点</h1>
</template>

常用 Composition API

setup

  • setup 是 Vue3 中一个新的配置项,值为函数
  • 组件中使用的数据、方法等都要配置在 setup 中
  • setup 函数两种返回值:
    • 返回一个对象,对象中的属性、方法可在模板中直接使用
    • 返回一个渲染函数,可自定义渲染内容
  • setup 函数的参数:
    • props:值为对象,包含了组件外部传进来,且组件内部声明接收的属性
    • context:上下文对象
      • attrs:值为对象,包含了组件外部传进来,且组件内部没有声明接收的属性,相当于 this.$attrs
      • slots:收到的插槽内容,相当于 this.$slots
      • emit:触发自定义事件的函数,相当于 this.$emit
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
// 没错,渲染函数就叫 h
import { h } from 'vue'

export default {
name: 'App',
props: ['title'],
// Vue3 需要声明自定义事件,虽然不声明也能运行
emits: ['changeCount'],

// 返回函数
/*
setup() {
return () => h('h1', 'Hello')
},
*/

// 返回对象
setup(props, context) {
let name = 'Vue3'
function sayHello() {}
function test() {
context.emit('changeCount', 888)
}

return {
name,
sayHello,
test,
}
},
}

注意:

  • setup 在 beforeCreate 钩子之前执行,thisundefined
  • setup 不要和 Vue2 配置混用。Vue2 的配置可以访问到 setup 的属性方法,反过来不行;如有重名,setup 优先
  • setup 不能是 async 函数,因为 async 函数返回的是 promise 不是对象,会导致模板无法访问属性方法
  • 若要返回 promise 实例,需要 Suspense 和异步组件的配合

ref 函数

作用:定义响应式数据

语法:const name = ref(initValue)

  • ref 函数返回一个 RefImpl(reference implement) 实例对象,全称引用实现的实例对象
  • 它包含响应式数据,简称引用对象、reference 对象、ref 对象
  • JS 访问数据:name.value
  • 模板访问数据:<div>{{ name }}</div>

注意事项:

  • ref 函数可以接收基本数据类型和引用数据类型
  • 基本类型数据的响应式还是靠 Object.defineProperty() 完成
  • 对象类型数据使用 ES6 的 Proxy 实现响应式,Vue3 把相关操作封装在 reactive 函数中
  • 按照之前的办法,对于对象数据,应该遍历每一层的属性添加 gettersetter,但 Vue3 使用 Proxy 把内部数据一口气监测了
1
2
<h2>{{ name }}</h2>
<p>{{ jobInfo.type }}</p>
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 { ref } from 'vue'

export default {
setup() {
let name = ref('Vue3')
let jobInfo = ref({
type: 'frontend',
salary: '40w',
})

function changeInfo() {
name.value = '鱿鱼丝'
// jobInfo 是 RefImpl 实例
// jobInfo.value 是 Proxy 实例对象
jobInfo.value.salary = '50w'
}

return {
name,
jobInfo,
changeInfo,
}
},
}

reactive 函数

  • 定义引用类型的响应式数据,不可用于 jibenleixingshuju
  • const 代理对象 = reactive(源对象) 接收对象或数组,返回代理对象(Proxy 的实例对象)
  • reactive 的响应式是深度的
  • 基于 ES6 的 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
import { reactive } from 'vue'

export default {
setup() {
let person = reactive({
name: 'Vue3',
sex: 'unknown',
info: {
school: 'Oxford',
major: 'computer',
},
})

let color = reactive(['red', 'green', 'blue'])

function changeInfo() {
person.info.major = 'art'
color[0] = 'yellow'
}

return {
person,
color,
changeInfo,
}
},
}

ref VS reactive

定义数据:

  • ref 用于定义基本类型数据
  • reactive 用于定义对象或数组类型数据
  • ref 也可定义对象或数组类型数据,内部通过 reactive 转为代理对象
  • 一般使用 reactive 函数,可以把所有数据封装为一个对象

原理:

  • ref 通过 Object.defineProperty() 实现响应式
  • reactive 通过 Proxy 实现响应式,Reflect 操作源对象数据

使用角度:

  • ref 定义数据,访问数据需要 .value,模板中不需要
  • reactive 定义的数据,都不需要

Vue3 响应式原理

  • 通过 Proxy 代理,拦截对对象属性的操作,包括增删改查
  • 通过 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
let originPerson = {
name: 'Lily',
age: 22,
}

let person = new Proxy(originPerson, {
// 拦截增加和查询操作
get(target, prop) {
// 读取源对象的属性
return Reflect.get(originPerson, prop)
},
// 拦截修改操作
set(target, prop, value) {
// 修改源对象的属性
return Reflect.set(target, prop, value)
},
// 拦截删除操作
deleteProperty(target, prop) {
// 删除源对象的属性
return Reflect.deleteProperty(target, prop)
},
})

console.log(person.name)
person.age = 33
person.sex = 'unknown'
delete person.age

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
import { reactive, computed } from 'vue'

export default {
setup() {
let person = reactive({
firstName: 'Cai',
lastName: 'QP',
})

// 计算属性简写形式
person.fullName = computed(() => {
return person.firstName + '-' + person.lastName
})

// 计算属性完整形式
person.fullName = computed({
get() {
return person.firstName + '-' + person.lastName
},
set(value) {
const arr = value.split('-')
person.firstName = arr[0]
person.lastName = arr[1]
},
})

return {
person,
}
},
}

watch 函数

:::tip Vue3 watch 能侦听的东西
A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types
:::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { ref, reactive, watch } from 'vue'
...

// 数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

侦听 ref 定义的响应式数据:

  • 注意不要写成 sum.value
1
2
3
4
5
6
7
8
// 参数:侦听的数据,回调,其他配置
watch(
sum,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ immediate: true }
)

侦听多个 ref 定义的响应式数据:

1
2
3
4
// newVal,oldVal 也是数组
watch([sum, msg], (newVal, oldVal) => {
console.log(newVal, oldVal)
})

侦听 ref 定义的对象类型数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 用 ref 定义对象类型数据
let person = ref({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

// 开启深度监听才有效,此时监听的是 RefImpl 实例
// Ref 实例的 value 是 Proxy 对象,存的是地址
// 因此无法监听 person 内部属性的变化
watch(person, (newVal, oldVal) => { ... }, { deep:true })

// 这个和 “侦听 reactive 函数直接返回的那一整坨响应式数据” 效果一致
watch(person.value, (newVal, oldVal) => {...})

侦听 reactive 函数直接返回的那一整坨响应式数据:

  • oldVal 是错误的!和 newVal 的值一样
  • 强制开启了深度侦听,deep 配置不生效!
1
2
3
4
5
6
7
watch(
person,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ immediate: true, deep: false }
)

侦听 reactive 定义的响应式数据某个属性:

  • 如果是 () => person.info oldVal 也是错误的!
  • () => person.name oldVal 是正确的,何时对何时错自己琢磨吧!
  • 此处没有强制开启深度监听
1
2
3
4
5
6
7
8
// 如果监视的属性还是对象,则需要开启深度监听
watch(
() => person.info,
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ deep: true }
)

侦听 reactive 定义的响应式数据多个属性:

1
2
3
4
5
6
7
watch(
[() => person.name, () => person.info],
(newVal, oldVal) => {
console.log(newVal, oldVal)
},
{ deep: true }
)

watchEffect 函数

  • watchEffect 不需要指明监听哪个属性,回调里用到哪个属性,就自动监听哪个属性
  • computed 注重计算的值,即回调函数的返回值,因此必须有返回值
  • watchEffect 更注重过程,即回调函数的函数体,因此可没有返回值
  • watchEffect 没有开启深度监听,也不能开启深度监听!
  • watchEffect 内部自行修改数据,不会重新调用回调,因此不会出现递归调用
1
2
3
4
5
6
// 回调中用到的数据只要发生变化,则直接重新执行回调
watchEffect(() => {
let total = sum.value
let p = person
console.log('watchEffect...')
})

生命周期

注意和 vue2.x 的生命周期图作对比,beforeDestroydestroyed 变为 beforeUnmountunmounted

vu3-lifecycle

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

  • beforeCreate===>setup()
  • created=======>setup()
  • beforeMount ===>onBeforeMount
  • mounted=======>onMounted
  • beforeUpdate===>onBeforeUpdate
  • updated =======>onUpdated
  • beforeUnmount ==>onBeforeUnmount
  • unmounted =====>onUnmounted

若和配置项生命钩子一起使用,则组合式会比配置项的先执行,如 onBeforeMount 先于 beforeMount

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
import { onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

setup(){
console.log('---setup---')
let sum = ref(0)

//通过组合式API的形式去使用生命周期钩子
onBeforeMount(()=>{
console.log('---onBeforeMount---')
})
onMounted(()=>{
console.log('---onMounted---')
})
onBeforeUpdate(()=>{
console.log('---onBeforeUpdate---')
})
onUpdated(()=>{
console.log('---onUpdated---')
})
onBeforeUnmount(()=>{
console.log('---onBeforeUnmount---')
})
onUnmounted(()=>{
console.log('---onUnmounted---')
})

return {sum}
},

hook 函数

  • hook 是一个函数,把 setup 函数的 Composition API 进行了封装
  • 类似 Vue2 的 Mixin,能复用代码,让 setup 里的逻辑更清晰
  • hook 放在 hooks 文件夹中,一个文件对应一个功能模块,以 useXxx 命名
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
// hooks/usePoint.js

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
}

//实现鼠标“打点”相关的生命周期钩子
onMounted(() => {
window.addEventListener('click', savePoint)
})

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

return point
}
1
2
3
4
5
6
7
8
9
10
// 使用 hook
import usePoint from '../hooks/usePoint.js'

export default {
setup() {
let point = usePoint()

return { point }
},
}

toRef 函数

  • 创建一个 RefImpl 实例对象,其 value 值指向另一个对象的某个属性,修改 value 值会修改源对象对应的属性
  • 应用:需要把响应式对象的某个属性单独提供给外部使用
  • 批量创建:toRefs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {reactive, toRef, toRefs} from 'vue'
...

setup() {
let person = reactive({
name: 'Vue3',
age: 18,
info: {
job: {
salary: 40,
},
},
})

return {
// 注意不能写成 ref(person.name),这和源对象是割裂开的
name: toRef(person, 'name'),
salary: toRef(person.info.job, 'salary')
// or
...toRefs(person)
}
}

其它 Composition API

shallowReactive & shallowRef

  • shallowReactive:只处理对象最外层属性的响应式,即浅响应式
  • shallowRef:基本数据类型和 ref 相同,对象数据不再会调用 reactive,因此只有对象引用改变了才是响应式的
  • 若一个对象数据,结构很深,但只有最外层属性变化,可用 shallowReactive
  • 若一个对象数据,属性不会改变,而是使用新对象替换,可用 shallowRef
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { shallowReactive, shallowRef } from 'vue'

setup() {
let person = shallowReactive({
name: 'Vue3',
age: 21,
info: {
job: {
salary: 22
}
}
})
let x = shallowRef({
y: 0
})
return {
person,
x
}
}

readonly & shallowReadonly

  • readonly: 让一个响应式数据变为只读的(深只读)
  • shallowReadonly:让一个响应式数据变为只读的(浅只读)
  • 应用场景: 不希望数据被修改时,如你用了别人的响应式数据,但是别人不希望你修改时
1
2
3
4
5
6
7
8
9
10
11
12
setup() {
let sum = ref(0)
let person = reactive({...})

sum = readonly(sum)
person = shallowReadonly(person)

return {
sum,
person
}
}

toRaw & markRaw

toRaw

  • 将一个由 reactive 生成的响应式对象转为普通对象
  • 用于读取响应式对象对应的普通对象,对该普通对象的操作不会引起页面更新

markRaw

  • 标记一个对象,让其不成为响应式对象
  • 有些值不应设置为响应式,比如复杂的第三方库
  • 当渲染复杂且不变的数据时,跳过响应式转换可提高性能

注意:仅仅让数据变为非响应式的,数据变的依旧变,只是没引起页面更新

1
2
3
4
5
6
7
8
9
10
11
12
13
setup() {
function showRawPerson() {
const p = toRaw(person);
p.age++;
console.log(p);
console.log(person);
}

function addCar() {
let car = { name: "奔驰", price: 40 };
person.car = markRaw(car);
}
}

customRef

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

1
2
<input type="text" v-model="keyword" />
<h3>{{ keyword }}</h3>
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
import { ref, customRef } from 'vue'

export default {
name: 'Demo',
setup() {
// 自定义 myRef
function myRef(value, delay) {
let timer
// 通过customRef去实现自定义
return customRef((track, trigger) => {
//
return {
get() {
//告诉Vue这个value值是需要被“追踪”的
track()
return value
},
set(newValue) {
clearTimeout(timer)
timer = setTimeout(() => {
value = newValue
// 告诉Vue去更新界面
trigger()
}, delay)
},
}
})
}
let keyword = myRef('hello', 500)
return {
keyword,
}
},
}

provide / inject

实现祖先组件与后代组件之间通信。

provide-inject

1
2
3
4
5
6
7
8
9
10
// 祖先组件传递数据
import { provide, reactive, ref } from 'vue'

setup() {
let car = reactive({...})
let sum = ref(0)

provide('sum', sum)
provide('car', car)
}
1
2
3
4
5
6
7
8
// 后代组件接收数据
import { inject } from 'vue'

setup() {
const car = inject('car')
const sum = inject('sum')
return { car, sum }
}

响应式数据的判断

  • isRef: 检查一个值是否为一个 ref 对象
  • isReactive: 检查一个对象是否是由 reactive 创建的响应式代理
  • isReadonly: 检查一个对象是否是由 readonly 创建的只读代理
  • isProxy: 检查一个对象是否是由 reactive 或者 readonly 方法创建的代理

Compositon API 的优势

Options API 存在的问题

使用传统 Options API 中,新增或者修改一个需求,就需要分别在 data,methods,computed 等地方修改。

 

Composition API 的优势

可以更加优雅地组织代码、函数,让相关功能的代码更加有序的组织在一起。说白了就是让同一个功能的代码整合到一起,日后修改代码直接找对应的功能模块。

 

新的组件

Fragment

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

Teleport

  • 将包裹的 HTML 结构移动到指定元素的末尾
  • to 属性为 CSS 选择器

简易的模态框效果:

1
2
3
4
5
6
7
8
<teleport to="#root">
<div v-if="isShow" class="mask">
<div class="dialog">
<h3>我是一个弹窗</h3>
<button @click="isShow = false">关闭弹窗</button>
</div>
</div>
</teleport>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.mask {
/* 遮罩层铺满窗口 */
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
}
.dialog {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
width: 300px;
height: 300px;
background-color: green;
}

Suspense

等待异步组件时渲染额外内容,让用户体验更好

异步引入组件:

1
2
import { defineAsyncComponent } from 'vue'
const Child = defineAsyncComponent(() => import('./components/Child.vue'))

使用 Suspense 包裹组件,实际上是往插槽填充内容,default 插槽填充组件内容,fallback 插槽填充组件加载时显示的内容:

1
2
3
4
5
6
7
8
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<h3>加载中,请稍等...</h3>
</template>
</Suspense>

另外,若 Child 组件的 setup 函数返回一个 Promise 对象,也能渲染 fallback 里的内容:

1
2
3
4
5
6
7
8
async setup() {
let sum = ref(0)
return await new Promise((resolve, reject) => {
setTimeout(() => {
resolve({sum})
}, 3000)
})
}

其他改变

  • 全局 API 的转移

Vue3 将全局的 API,即:Vue.xxx 调整到应用实例 app 上:

Vue2 全局 API Vue3 实例 API
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
  • data 选项应始终被声明为一个函数

  • 过渡类名的更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Vue2 */
.v-enter,
.v-leave-to {
opacity: 0;
}
.v-leave,
.v-enter-to {
opacity: 1;
}

/* Vue3 */
.v-enter-from,
.v-leave-to {
opacity: 0;
}

.v-leave-from,
.v-enter-to {
opacity: 1;
}
  • 移除 keyCode 作为 v-on 的修饰符,同时也不再支持 config.keyCodes
  • 移除 v-on.native修饰符,子组件没有在 emits: ['close'] 声明的自定义事件作为原生事件处理
  • 移除过滤器 filter

组件上的 v-model

当需要维护组件内外数据的同步时,可以在组件上使用 v-model 指令。

父组件传值:

1
2
<!-- 父组件传值 -->
<my-counter v-model:number="count"></my-counter>

子组件在 emits 节点声明自定义事件,格式为 update:xxx ,调用 $emit 触发自定义事件:

1
2
3
4
5
6
7
8
9
export default {
props: ['number'],
emits: ['update:number'],
methods: {
add() {
this.$emit('update:number', this.number++)
},
},
}

注意,在 vue3props 属性同样是只读的,上面 this.number++ 并没有修改 number 的值。

其实通过 v-bind 传值和监听自定义事件的方式能实现和 v-model 相同的效果。

EventBus

借助于第三方的包 mitt 来创建 eventBus 对象,从而实现兄弟组件之间的数据共享。

安装 mitt 依赖包:

1
npm install mitt@2.1.0

创建公共的 eventBus 模块:

1
2
3
4
5
6
import mitt from 'mitt'

// 创建 EventBus 实例对象
const bus = mitt()

export default bus

数据接收方调用 bus.on() 监听自定义事件:

1
2
3
4
5
6
7
8
9
10
11
12
import bus from './eventBus.js'

export default {
data() {
return { count: 0 }
},
created() {
bus.on('countChange', (count) => {
this.count = count
})
},
}

数据接发送方调用 bus.emit() 触发事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
import bus from './eventBus.js'

export default {
data() {
return { cout: 0 }
},
methods: {
addCount() {
this.count++
bus.emit('countChange', this.count)
},
},
}

vue 3.x 全局配置 axios

实际项目开发中,几乎每个组件中都会使用 axios 发起数据请求。此时会遇到如下两个问题:

  • 每个组件中都需要导入 axios(代码臃肿)
  • 每次发请求都需要填写完整的请求路径(不利于后期的维护)

main.js 文件中进行配置:

1
2
3
4
5
6
// 配置请求根路径
axios.defaults.baseURL = 'http://api.com'

// 将 axios 挂载为 app 全局自定义属性
// 每个组件可通过 this.$http 访问到 axios
app.config.globalProperties.$http = axios

组件调用:

1
this.$http.get('/users')