vue3新特性简介

Compiler 原理篇

  • 静态 Node 不再作更新处理( hoiststatic->SSR 优化)

  • 静态绑定的 class,id 不再作更新处理

  • 结合打包标记 Patchflag,进行更新分析(动态绑定)

  • 事件监听器 Cache 缓存处理(cacheHandlers)

  • hoiststatic 自动针对多静态节点进行优化,输出字符串

vue2 中代码复用方法,如: Mixin, Filters 都有缺陷

  • Mixin(命名空间冲突、逻辑不清晰、不易复用)
  • scoped slot 作用域插槽(配置项多、代码分裂、性能差)
  • vue2 对 TS 支持不充分

首先,先来看一些小的变化

  • 生命周期更名:
    destroyed –> unmounted
    beforeDestroy –> beforeUnmount

  • data 选项始终声明为函数

  • 删除 $on, $off$once API

  • 删除 Filters API,改用 methodcomputed 替换

  • v-on 不再支持使用数字(即 keyCodes)作为修饰符,config.keyCodes 不再受支持

  • v-enter 过渡类已重命名为 v-enter-fromv-leave 过渡类已更名为 v-leave-from

接下来看一些 Composition API 的更改:

为什么要用 Composition API?

组合式 API + 函数式编程(复杂组件逻辑进行分离)
组件间逻辑共享

Vue3.0 带来的变化

  • 性能提升 1.3 ~ 2x
  • TS 支持,新增: Fragment、 Teleport、 Suspense
  • 按需加载(配合 vite) & 组合 API

Fragment: 不受根节点限制,渲染函数可接收 Array
Teleport 类似 Portal,随用随取,eg. 弹窗,Actions
Suspense 嵌套的异步依赖,eg. async setup()

vue2 对于复杂逻辑组件,在后期变得无法维护。逻辑被拆分成:

  • components
  • props
  • data
  • computed
  • methods
  • 生命周期的方法

Scaffold

vite:

1
npm init vite-app hello-vue3

vue-cli:

1
2
npm install -g @vue/cli
vue create hello-vue3

注:尤大写的 vite 捆绑了 rollup 进行打包,而且具有以下特性:

  • 快速启动冷服务器
  • 即时热模块更换(HMR)
  • 真正的按需编译
  • 更详细的日志信息

v-for Array Refs

v-for 循环绑定的 ref 可以更加灵活,且定义非常方便

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<template>
<div v-for="item in list" :ref="setItemRef"></div>
</template>

export default {
data() {
return {
list: []
itemRefs: []
}
},
methods: {
setItemRef(el) {
this.itemRefs.push(el)
}
},
beforeUpdate() {
this.itemRefs = []
},
updated() {
console.log(this.itemRefs)
}
}

defineAsyncComponent

定义异步组件

1
2
3
4
5
6
7
8
9
10
11
12
13
import { defineAsyncComponent } from "vue";
import ErrorComponent from "./components/ErrorComponent.vue";
import LoadingComponent from "./components/LoadingComponent.vue";

const asyncPage = defineAsyncComponent(() => import("./NextPage.vue"));

const asyncPageWithOptions = defineAsyncComponent({
loader: () => import("./NextPage.vue"),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent,
});

Custom Directives

v-for 循环绑定的 ref 可以更加灵活,切定义非常方便

bind: 一旦该指令被绑定到元素时执行。仅执行一次。
inserted: 在将元素插入父 DOM 中时执行。
update: 元素更新时调用此挂钩,但是子组件尚未更新。
componentUpdated: 组件和子组件更新后,将调用此挂钩。
unbind: 删除指令后将调用此钩子。也仅调用一次。

bind 更名为 beforeMount
inserted 更名为 mounted
beforeUpdate: 这在元素本身更新之前被调用,就像组件生命周期挂钩一样。
update 已删除
componentUpdated 更名为 updated
beforeUnmount 与组件生命周期挂钩类似,这将在卸载元素之前立即调用。
unbind 更名为 unmounted

Data Option

合并 data 来自 mixin 或扩展的多个返回值时,合并现在较浅而不是较深(仅合并了根级属性)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const Mixin = {
data() {
return {
user: {
name: "Jack",
id: 1,
},
};
},
};

const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2,
},
};
},
mounted() {
console.log(this.$data); // {user: { id: 2 }}
},
};

Fragments

组件现在可以具有多个根节点!但是,这确实需要开发人员明确定义属性应在何处分发。

1
2
3
4
5
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>

Global API

新增 createApp 返回一个应用程序实例,现在可以将全局更改 Vue 行为的所有 API 移至应用程序实例。

1
2
import { createApp } from "vue";
const app = createApp({});
2.x Global API3.x Instance API
Vue.configapp.config
Vue.config.productionTip已移除
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use

key attribute

1
2
3
4
5
<!-- key 应该将放在 -->
<template v-for="item in list" :key="item.id">
<div>...</div>
<span>...</span>
</template>

v-model

在自定义组件上使用时,v-modelpropevent 的默认名称已更改:

prop value-> modelValue
event input-> update: modelValue
v-model 现在可以在同一组件上进行多个绑定;
v-bind.sync 修饰符和组件 model 选项已删除,并替换为 v-model 的一个参数;
添加了创建自定义 v-model 修饰符的功能;

1
<ChildComponent v-model="pageTitle" />
1
2
3
4
5
6
7
8
9
10
11
// ChildComponent
export default {
props: {
modelValue: String,
},
methods: {
changePageTitle(title) {
this.$emit("update:modelValue", title);
},
},
};

最后,我们再来看一些重大的变化

reactive

Vue3 响应式实现原理是通过 ES6 的 Proxy 实现的,但是对于 IE 浏览器,Vue3 也使用了 Object.defineProperty。
vue3 将 Vue.observable()重命名为 reactive,并提供了单独的分离。

1
2
3
4
5
import { reactive } from "vue";

const state = reactive({
count: 0,
});

ref

对于一个独立的原始值,Vue3 也提供了对应的响应式 API。

  • ref 包裹的变量(可以是任意类型)都是深度响应式的
  • 通过 .value 取值和赋值
  • 顶层 property 在 template 中自动解包,不需要 .value 取值
1
2
3
4
5
6
7
import { ref } from "vue";

const count = ref(0);
console.log(count.value); // 0

count.value++;
console.log(count.value); // 1
1
2
3
4
5
6
7
// 解包过程仅作用于顶层 property,访问深层级的 ref 则不会解包
const object = { foo: ref(1) };
// {{ object.foo }} //无法自动解包

// 通过让 foo 成为顶层 property 来解决这个问题
const { foo } = object;
// {{ foo }} //自动解包

响应性语法糖

基于:vue@^3.2.25 @vitejs/plugin-vue@^2.0.0

1
2
3
4
5
6
7
8
// vite.config.js
export default {
plugins: [
vue({
reactivityTransform: true,
}),
],
};

取值不需要 .value

1
2
3
4
5
6
7
8
9
10
11
<script setup>
let count = $ref(0);

function increment() {
count++; // no need for .value
}
</script>

<template>
<button @click="increment">{{ count }}</button>
</template>

toRefs

ES6 解构会破坏响应式,对于这种情况可以使用 toRefs 去避免。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { reactive, toRefs } from "vue";

const book = reactive({
author: "Vue Team",
year: "2020",
title: "Vue 3 Guide",
description: "You are reading this book right now ;)",
price: "free",
});

let { author, title } = toRefs(book);

title.value = "Vue 3 Detailed Guide";
console.log(book.title); // 'Vue 3 Detailed Guide'

readonly

有时我们想避免响应式更改,可以使用 readonly。

1
2
3
4
5
6
7
8
9
import { reactive, readonly } from "vue";

const original = reactive({ count: 0 });

const copy = readonly(original);

original.count++;

copy.count++; // warning: "Set operation on key 'count' failed: target is readonly."

computed

有时我们需要依赖于其他状态的状态,可以通过计算属性来处理的。

  • 默认返回 readonly 的 ref 值,需要 .value 获取
  • 可设置 writable 方式
1
2
3
4
5
6
7
8
9
10
const count = ref(1);
const plusOne = computed({
get: () => count.value + 1,
set: (val) => {
count.value = val - 1;
},
});

plusOne.value = 1;
console.log(count.value); // 0

watch 和 watchEffect

区别:

  • watch  只追踪明确侦听的源。它不会追踪任何在回调中访问到的东西。另外,仅在响应源确实改变时才会触发回调。watch  会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式 property。这更方便,而且代码往往更简洁,但其响应性依赖关系不那么明确。

watch

  • 默认懒监听, 第一个参数要是个响应式变量,如果监听数值变化,用 getter
  • watch 的第二个参数为 callback,callback 参数第三个为 cleanup 函数,在下次变更前调用
  • watch 的第三个参数为配置项,包括
    • immediate : watch 创建后立马调用,oldValue 此时为 undefined
    • deep: 深度监听,用于监听第一个参数为 getter 的 响应式对象, 用于大型数据结构时,开销很大

监听一个 getter

1
2
3
4
5
6
// 监听一个getter,正常值
const state = reactive({ count: 0 });

setTimeout(() => {
state.count++;
}, 1000);
1
2
3
4
5
6
7
8
// 用 getter 函数
watch(
() => state.count,
(count, prevCount) => {
// count is: 1, prevCount is 0
console.log(`count is: ${count}, prevCount is ${prevCount}`);
}
);

错误:

这不会正常工作,因为是向 watch() 传入了一个 number

1
2
3
watch(state.count, (count) => {
console.log(`count is: ${count}`);
});

监听一个 getter 深层响应的对象

1
2
3
4
5
6
7
8
const state = reactive({ count: 0 });
watch(
() => state,
(newValue, oldValue) => {
// newValue === oldValue
},
{ deep: true } // 如果想深层监听,设置true,否则只有state替换掉才触发
);

直接监听深层响应的对象

自动默认 deep 为 true

1
2
3
4
const state = reactive({ count: 0 });
watch(state, () => {
/* triggers on deep mutation to state */
});

监听一个 ref

1
2
3
4
const count = ref(0);
watch(count, (count, prevCount) => {
/* ... */
});

监听多个 refs

1
2
3
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
});

flush

pre | post | sync
默认情况下,用户创建的侦听器回调,都会在 Vue 组件更新之前被调用。
在侦听器回调中能访问被 Vue 更新之后的 DOM,需要设置 post

1
2
3
watch(source, callback, {
flush: "post",
});

onTrack / onTrigger

1
2
3
4
5
6
7
8
watch(source, callback, {
onTrack(e) {
debugger;
},
onTrigger(e) {
debugger;
},
});

watchEffect

为了监听响应式更改,可以使用该 watchEffect 方法,它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。它会返回一个停止函数,通过显式调用可以停止侦听。

特性:

  • 立即执行
  • 第一个参数是执行函数,函数的第一个参数是清除函数(下一次触发执行函数时调用)
  • 第二个参数为配置项
1
2
3
4
5
6
7
8
9
10
11
12
const count = ref(0);

const stop = watchEffect(() => console.log(count.value));
// -> logs 0

setTimeout(() => {
count.value++;
// -> logs 1
}, 100);

// later
stop();

清除函数使用

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
const id = $ref(1);
let timer = setInterval(() => {
id++;
if (id === 5) {
clearInterval(timer);
id++;
}
}, 1000);

// 主功能函数
watchEffect(async (onCleanup) => {
// 只要id变化就请求
const { response, cancel } = doAsyncWork(id);
//如果没完成,可以取消之前的请求
onCleanup(cancel);
console.log("前五秒不会触发请求");
let data = await response;
console.log(data);
});
// 模拟的请求操作
function doAsyncWork() {
let time = null;
let response = new Promise((resolve, reject) => {
// 假设请求很慢,三秒才回来 可以做axios请求 ,
time = setTimeout(() => {
resolve("hello");
}, 3000);
});
return {
response,
cancel: () => {
response = null;
clearTimeout(time);
},
};
}

侦听器必须用同步语句创建

如果用异步回调创建一个侦听器,则不会绑定到当前组件上,必须手动停止它

1
2
3
4
5
6
7
8
9
10
11
12
<script setup>
import { watchEffect } from "vue";
// 它会自动停止
watchEffect(() => {});

// ...这个则不会!
setTimeout(() => {
const unwatch = watchEffect(() => {});
// ...当该侦听器不再需要时
unwatch();
}, 100);
</script>

setup

setup 将可重复部分及其功能提取到可重用的代码段中。
setup 选项接受 props 和 context 的函数,返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div>{{ readersNumber }} {{ book.title }}</div>
</template>

import { ref, reactive } from 'vue'

export default {
setup(props, context) {
const readersNumber = ref(0)
const book = reactive({ title: 'Vue 3 Guide' })

// expose to template
return {
readersNumber,
book
}
}
}

Provide / Inject

  • 在 setup() 中使用 provide 时,我们首先从 vue 显式导入 provide 方法。这使我们能够调用 provide 时来定义每个 property。
  • provide 函数允许你通过两个参数定义 property:
  • property 的 name ( 类型)
  • property 的 value
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// src/components/MyMap.vue
<template>
<MyMarker />
</template>

import { provide } from 'vue'
import MyMarker from './MyMarker.vue

export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}

在 setup() 中使用 inject 时,还需要从 vue 显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。

  • inject 函数有两个参数:
    • 要注入的 property 的名称
    • 一个默认的值 (可选)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/components/MyMarker.vue
import { inject } from "vue";

export default {
setup() {
const userLocation = inject("location", "The Universe");
const userGeolocation = inject("geolocation");

return {
userLocation,
userGeolocation,
};
},
};