从 Vue2 到 Vue3
截止2023年12月31日,Vue2 已经终止支持且不再维护(详见官方文档)
API
从 Vue2 迁移到 Vue3,最直观的感受无疑是 API 风格的变化。
模块导入
首先是模块导入上的变化,从下例中可以看到,Vue2 需要将整个模块一起导入,所有的操作基本都通过Vue
来完成。而 Vue3 则是单独导入需要的模块,这种做法支持摇树优化(Tree Shaking),在打包时没有使用的模块则会被移除,大大减小了打包体积。
import Vue from "vue";
const app = new Vue({ el: "#app", /* 根组件选项 */ });
const MyComp = Vue.component("MyComp", { /* 组件选项 */ });
2
3
4
5
import { createApp } from "vue";
const app = createApp({ /* 根组件选项 */ });
app.mount("#app"); // 挂载应用到 #app
app.component("MyComp", /* 组件对象 */);
2
3
4
5
6
组合式 API
观察生命周期的差别,我们可以发现 Vue3 在组件实例创建之前多了一个环节setup
,我们提到的“组合式 API”就是在这一阶段生效。
Vue2 生命周期
Vue3 生命周期
在实现较为复杂的组件时,我们的功能模块通常会分布在选项式 API 的各个地方(数据在data
中,核心的逻辑处理在methods
中,而计算属性则在computed
中等等),当组件实现了的多种功能之后,它们的代码交织在一起,变得非常难以阅读和维护。
相比之下,组合式 API 则允许我们在setup
中完成对数据、方法、生命周期钩子等等的各种操作,使得我们可以将相关的逻辑都放在一起进行处理,更加适合大型组件的开发与维护。
setup 用法
<script>
import { ref } from "vue";
export default {
setup() {
const count = ref(0);
// 返回值会暴露给模板和其他的选项式 API
return { count };
},
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
选项式 API 对比组合式 API
在setup
中返回的对象会暴露给模板和组件实例,因此在使用setup
的时候我们通常不再需要去定义data
、methods
等选项,仅仅使用setup
就可以完成组件的定义,所以 Vue3 提供了一种更为简洁的写法(适用于单文件组件):
<script>
import { ref } from "vue";
export default {
setup() {
const count = ref(0);
// 返回值会暴露给模板和其他的选项式 API,并且 ref 值会自动解包
return { count };
},
mounted() {
console.log(this.count);
},
}
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, onMounted } from "vue";
// 模板中可以直接使用定义的所有变量、函数和 import 导入的内容
const count = ref(0);
// 可以在 setup 中使用生命周期钩子函数
onMounted(() => {
console.log(count.value);
});
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
setup
中无法访问组件实例,因此不应该在setup
中使用this
(通常为undefined
)
更优秀的 TS 支持
TypeScript 这样的类型系统可以在编译时分析出很多常见的错误,同时 IDE 也可以基于类型提供更好的开发体验和效率。
Vue3 使用 TypeScript 编写,并为对象和函数都提供了优秀的 TypeScript 支持,所有的官方库都自带了类型声明文件,因此使用任何的官方组件,都可以获得完整的类型提示(只要你的 IDE 支持,不论你是用的是 JS 还是 TS)。
下例展示了 Vue3 对于props
的类型支持:
<script setup>
// defineProps 宏支持基于传入的参数进行类型推导
const props = defineProps({
foo: { type: String, required: true },
bar: Number
});
props.foo; // string
props.bar; // number | undefined
</script>
2
3
4
5
6
7
8
9
10
<script setup lang="ts">
// 通过泛型更加直接、精确地定义想要的类型
const props = defineProps<{
foo: string
bar?: number
}>();
props.foo; // string
props.bar; // number | undefined
</script>
2
3
4
5
6
7
8
9
10
响应式变更
从 Vue2 迁移到 Vue3,响应式的变更无疑是非常重要且关键的。
Object.defineProperty
在 Vue2 时期,为了给旧版本的浏览器提供支持,Vue2 使用Object.defineProperty
来劫持对象属性的get
和set
行为,在get
时记录依赖,在set
时派发更新,从而实现数据的响应式变化。
由于每次使用Object.defineProperty
只针对一个属性生效,所以 Vue2 对于要进行响应式处理的对象,需要在使用前遍历对象中的每一个属性,通过Object.defineProperty
来劫持针对该属性的操作,如果该属性是个对象,还需要递归遍历其属性。这种做法首先非常影响效率,因为 Vue2 自身无法判断一个属性是否可能被使用,为了避免没有添加响应式导致超出用户预期的情况,Vue2 只能在使用前把所有的属性都完成修改。
使用Object.freeze
冻结对象可以让 Vue2 不为对象添加响应式
Proxy
ES6 中提出了一个新的特性Proxy
,通过Proxy
,我们可以得到一个对象(不能是string
、number
等原始类型,必须为object
)的代理,通过这个代理,我们可以劫持对象的各种行为,不再局限于get
、set
对象属性,还可以支持属性的新增、删除等,对于数组的push
等方法也不在话下。可以说通过Proxy
,我们获得了对对象极高自由度的控制,而 Vue3 的响应式正是基于Proxy
实现。
WARNING
Proxy
会生成一个新的对象,因此以下等式皆不成立(为false
):
obj === new Proxy(obj, { /* ... */ })
new Proxy(obj, {}) === new Proxy(obj, {})
此外,Proxy
可以劫持对对象深层属性的操作,基于这一特性,Vue3 不再需要在使用对象前递归处理每一个属性,只需要将最外部的对象转换成代理对象即可,当访问到某个属性时,再进行额外的处理。(示例见#reactive)
reactive
Vue3 的组合式 API 提供了reactive
函数,可以返回一个原始对象的Proxy
,当代理对象改变时,会根据依赖派发更新。
由于Proxy
的特性,有以下几点需要注意:
- 对象的对象属性只有在被访问到时,才会被转换为代理对象(惰性响应式)
- 原始对象与代理对象并非同一个,所以对原始对象的修改不会触发更新
- 被解构的属性会丢失响应式,因为响应式定义在
proxy
对象上,而不是在属性上
Proxy
深层监听示例(仅get
)
// 通过这些特定的标记,可以访问响应式对象的特殊属性
enum ReactiveFlags {
IS_REACTIVE = "__is_reactive__",
RAW = "__raw__",
}
interface Target {
[ReactiveFlags.IS_REACTIVE]: boolean;
[ReactiveFlags.RAW]: boolean;
}
export function isReactive(value: object): boolean {
return !!value[ReactiveFlags.IS_REACTIVE];
}
// 使用 WeakMap,避免影响垃圾回收
const proxyMap: WeakMap<object, any> = new WeakMap();
class ReactiveHandler implements ProxyHandler<Target> {
// 标记属性记录在 handler 上,避免对原始对象产生影响
protected readonly isReactive: boolean = true;
get(target: Target, key: string | symbol, receiver: any) {
if (key === ReactiveFlags.IS_REACTIVE) return this.isReactive;
// 返回原始对象
if (key === ReactiveFlags.RAW) return target;
const res = Reflect.get(target, key, receiver);
/* track(...) 收集依赖 */
// 递归建立监听(有别于 Object.defineProperty,仅在访问到时执行)
if (isObject(res) && !isReactive(res)) return reactive(res);
return res;
}
}
export function reactive<T extends object>(target: T): T {
if (!isObject(target)) throw "target must be an object";
// 不能使用 target instanceof Proxy 来判断 target 是否为 Proxy
// 因此采用新增属性的方式
if (isReactive(target)) return target;
if (!proxyMap.has(target)) {
const proxy = new Proxy(target as Target, new ReactiveHandler());
proxyMap.set(target, proxy);
}
return proxyMap.get(target)!;
}
/**
* 尝试获得响应式对象的原始对象,否则返回自身
*/
export function toRaw<T>(value: T): T {
const raw = value && value[ReactiveFlags.RAW];
return raw ? toRaw(raw) : value;
}
/**
* 尝试将 value 转变为 reactive,否则返回自身
*/
export function toReactive<T>(value: T): T {
return isObject(value) ? reactive(value) : value;
}
const a = { author: { name: "abc", age: 18 } };
const b = reactive(a);
console.log(b === reactive(a)); // true
console.log(b === reactive(b)); // true
console.log(b.author === reactive(a.author)); // true
console.log(b[ReactiveFlags.RAW] === a); // true
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
ref
由于reactive
基于Proxy
实现,只能实现对对象的监听,所以组合式 API 中提供了ref
函数,它可以实现对任意数据的响应式。
当然,由于 JS 本身并不支持对原始数据类型的变化监听,所以ref
的做法是将要监听的目标放在了一个对象的value
属性中,通过监听该对象的value
属性来实现响应式,并且如果目标值是一个对象,ref
本身会通过调用reactive
来实现对该对象的深度监听。
ref
实现任意类型的响应式
enum RefFlags {
IS_REF = "__is_ref__",
}
interface Ref<T = any> {
value: T;
}
function isRef<T>(value: Ref<T> | unknown): value is Ref<T> {
return !!(value && value[RefFlags.IS_REF]);
}
class RefImpl<T> implements Ref<T> {
[RefFlags.IS_REF]: boolean = true;
#raw: T;
#value: T;
constructor(value: T) {
this.#raw = toRaw(value);
this.#value = toReactive(value);
}
get value(): T {
/* track(...) 收集依赖 */
return this.#value;
}
set value(newVal: T) {
const raw = toRaw(newVal);
// 原始类型的值或对象的地址改变,需要更新
if (!Object.is(this.#raw, raw)) {
this.#raw = raw;
this.#value = toReactive(raw);
/* trigger(...) 派发更新 */
}
}
}
export function ref<T = any>(): Ref<T | undefined>;
export function ref<T>(target: T | Ref<T>): Ref<T>;
export function ref(value?: unknown) {
return isRef(value) ? value : new RefImpl(value);
}
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
框架的功能无法超出语言的边界,因此ref
必须包装成一个对象
性能提升
相比 Vue2,Vue3 的性能也得到了非常大的提升,这是多方面的努力带来的结果。
diff 算法
在 Vue 的template
中编写的代码并非真正要渲染的 HTML 代码,Vue 会在运行时根据模板动态地生成“虚拟 DOM 树”,然后将数据填充进去,在生成实际的 DOM 节点添加到 DOM 树中。而要实现响应式,就需要在数据发生变动时重新生成 VDOM,并更新 DOM 树。
Vue3 的 diff 算法可以更智能地发现需要更新的节点,减少不必要的遍历和检查。这是因为 Vue3 会在编译阶段,对可能发生变化的节点进行标记,用以指导后续的节点更新,为不同的情况做出最优的处理:
/**
* vue@3.4.21
* core/packages/shared/src/patchFlags.ts
*/
export enum PatchFlags {
TEXT = 1, // 动态的文本
CLASS = 1 << 1, // 动态的 class
STYLE = 1 << 2, // 动态的 style
PROPS = 1 << 3, // 动态的属性(除 class、style 外)
FULL_PROPS = 1 << 4, // 动态的属性名(例如:<comp :[key]="123" />),与 CLASS/STYLE/PROPS 标记互斥
NEED_HYDRATION = 1 << 5, // 复合属性(例如:v-on、带有修饰符的 v-bind 等)
STABLE_FRAGMENT = 1 << 6, // 片段的子节点顺序不会变动
KEYED_FRAGMENT = 1 << 7, // 片段的部分(或全部)子节点具有 key
UNKEYED_FRAGMENT = 1 << 8, // 片段的部分(或全部)子节点不具有 key
NEED_PATCH = 1 << 9, // 没有动态的属性,但是有 ref 或 directives 等
DYNAMIC_SLOTS = 1 << 10, // 组件具有动态插槽(例如:插槽名动态生成)
DEV_ROOT_FRAGMENT = 1 << 11, // 模板根节点具有注释(仅开发模式生效,生产环境会移除注释)
/* 特殊的负数标记 */
HOISTED = -1, // 静态提升标记:整个子树完全是静态的,不需要更新
BAIL = -2, // 放弃优化标记:例如是由用户手动编写的渲染函数生成的,这种情况下不应当优化,确保结果始终不同
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
静态提升
上面的标记中有一个特殊的HOISTED
标记,拥有这个标记的节点会被“静态提升”,因为这些节点的内容是完全静态的,不论渲染几次它们都是完全相同的,所以只需要生成一次,后续重复使用初次生成的节点即可。这一优化可以非常显著地提升大型应用的效率,优化运行时的内存占用,减少垃圾回收的次数。
TIP
静态提升不等于节点中没有动态插入的内容,只要插入的是常量,就可以被静态提升:
<script setup>
const content = "Hello, world!";
</script>
<template>
<span>{{ content }}</span>
</template>
2
3
4
5
6
7
预字符串化
当编译器遇到大量连续的静态内容时,编译器可以识别出这些内容,并将它们直接编译成一个字符串,在渲染到 DOM 树时直接进行拼接:
<div class="sidebar">
<div class="logo">
<h1 class="title">{{ title }}</h1>
</div>
<ul class="list">
<li>aaa</li>
<li>bbb</li>
<li>ccc</li>
<li>ddd</li>
<li>eee</li>
</ul>
<div class="footer">
<span>footer</span>
</div>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
上面这一个 DOM 树在 Vue3 中可能得到一个非常简洁的 VDOM 树:
总结
不只是上述提到的各种改变,Vue3 还有许多大大小小的变化,并且这是一个在不断进步的成熟开源框架,更多详细的信息都可以在官方文档和 Github 中找到:
- Vue3 文档:Vue.js
- 迁移指南:Vue 3 迁移指南
- Github:vuejs/core