Vue3 新特性 TODO: 性能提升
createApp
1// vue2
2const app = new Vue(options);
3// 使用插件:Vue.use()
4app.$mount("#app");
5// vue3
6// 不存在构造函数Vue
7const app = createApp(App);
8// 使用插件:app.use()
9app.mount("#app");
10// 简写:createApp(App).mount("#app");
11// App是根组件,返回一个vue应用
this 变化:vue3 的组件实例代理

composition api 组合 api
vue2 配置式的,data,computed,props,methods,即 option api
vue3 对 ref 的特殊处理

去掉了 Vue 构造函数
可以更好的 tree shaking
在过去,如果遇到一个页面有多个vue应用时,往往会遇到一些问题
1<!-- vue2 -->
2<div id="app1"></div>
3<div id="app2"></div>
4<script>
5 Vue.use(...); // 此代码会影响所有的vue应用
6 Vue.mixin(...); // 此代码会影响所有的vue应用
7 Vue.component(...); // 此代码会影响所有的vue应用
8 // 可能只是第一个需要用插件,第二个不用,之前毫无办法
9 new Vue({
10 // 配置
11 }).$mount("#app1")
12
13 new Vue({
14 // 配置
15 }).$mount("#app2")
16</script>
在vue3中,去掉了Vue构造函数,转而使用createApp创建vue应用
1<!-- vue3 -->
2<div id="app1"></div>
3<div id="app2"></div>
4<script>
5 去了vue实例里面 链式编程,因为每次都返回还是实例
6 createApp(根组件).use(...).mixin(...).component(...).mount("#app1")
7 createApp(根组件).mount("#app2")
8</script>
更多 vue 应用的 api:https://v3.vu ejs.org/api/application-api.html
组件实例中的 API
在vue3中,组件实例是一个Proxy,它仅提供了下列成员,功能和vue2一样
属性:https://v3.vuejs.org/api/instance-properties.html
方法:https://v3.vuejs.org/api/instance-methods.html
只能调用列表里面的属性和方法,私有的和其他的不可以调用
对比数据响应式
vue2 和 vue3 均在相同的生命周期(beforeCreate 后,created 之前)完成数据响应式,但做法不一样
:::tip
为什么 vue3 中去掉了 vue 构造函数?
:::
vue2 的全局构造函数带来了诸多问题:
- 调用构造函数的静态方法会对所有 vue 应用生效,不利于隔离不同应用
- vue2 的构造函数集成了太多功能,不利于 tree shaking,vue3 把这些功能使用普通函数导出,能够充分利用 tree shaking 优化打包体积
- vue2 没有把组件实例和 vue 应用两个概念区分开,在 vue2 中,通过 new Vue 创建的对象,既是一个 vue 应用,同时又是一个特殊的 vue 组件。vue3 中,把两个概念区别开来,通过 createApp 创建的对象,是一个 vue 应用,它内部提供的方法是针对整个应用的,而不再是一个特殊的组件。
vue3 不再使用 Object.defineProperty 的方式定义完成数据响应式,而是使用 Proxy。除了 Proxy 本身效率比 Object.defineProperty 更高之外,由于不必递归遍历所有属性,而是直接得到一个 Proxy。所以在 vue3 中,对数据的访问是动态的,当访问某个属性的时候,再动态的获取和设置,这就极大的提升了在组件初始阶段的效率。(想用那个再读那个),如果访问一个对象,则再返回一个 proxy。同时,由于 Proxy 可以监控到成员的新增和删除,因此,在 vue3 中新增成员、删除成员、索引访问等均可以触发重新渲染,而这些在 vue2 中是难以做到的。
vue2 的响应式是使用 Object.defineProperty 完成的,它会对原始对象有侵入。
在创建响应式阶段,会递归遍历原始对象的所有属性,当对象属性较多、较深时,对效率的影响颇为严重。不仅如此,由于遍历属性仅在最开始完成,因此在这儿之后无法响应属性的新增和删除。
在收集依赖时,vue2 采取的是构造函数的办法,构造函数是一个整体,不利于 tree shaking。
vue3 的响应式是使用 Proxy 完成的,它不会侵入原始对象,而是返回一个代理对象,通过操作代理对象完成响应式。
由于使用了代理对象,因此并不需要遍历原始对象的属性,只需在读取属性时动态的决定要不要继续返回一个代理,这种按需加载的模式可以完全无视对象属性的数量和深度,达到更高的执行效率。
由于 ES6 的 Proxy 可以代理更加底层的操作,因此对属性的新增、删除都可以完美响应。
在收集依赖时,vue3 采取的是普通函数的做法,利用高效率的 WeakMap 实现依赖记录,这利于 tree shaking,从而降低打包体积。
v-model
vue2比较让人诟病的一点就是提供了两种双向绑定:v-model和.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。
为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改
- 当对自定义组件使用
v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue
1<!-- vue2 -->
2<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
3value绑定一个数据,input的时候给这个数据重新赋值
4<!-- 简写为 -->
5<ChildComponent v-model="pageTitle" />
6
7<!-- vue3 -->
8<ChildComponent
9 :modelValue="pageTitle"
10 @update:modelValue="pageTitle = $event"
11/>
12<!-- 简写为 -->
13<ChildComponent v-model="pageTitle" />
- 去掉了
.sync修饰符,它原本的功能由v-model的参数替代
1<!-- vue2 -->
2<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
3<!-- 简写为 -->
4<ChildComponent :title.sync="pageTitle" />
5
6<!-- vue3 -->
7<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
8<!-- 简写为 -->
9<ChildComponent v-model:title="pageTitle" />
model配置被移除
- 允许自定义
v-model修饰符
vue2 无此功能
举例:trim

v-if v-for
v-if 的优先级 现在高于 v-for
key
- 当使用
<template>进行v-for循环时,需要把key值放到<template>中,而不是它的子元素中
- 当使用
v-if v-else-if v-else分支的时候,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key
即便要手工给予key值,也必须给予每个分支唯一的key,不能因为要重用分支而给予相同的 key
Fragment
vue3现在允许组件出现多个根节点
组件的变化
Teleport
asyncComponent 异步组件
CompositionApi
composition api 相比于 option api 有哪些优势?
不同于 reactivity api,composition api 提供的函数很多是与组件深度绑定的,不能脱离组件而存在。
setup
1// component
2export default {
3 setup(props, context) {
4 // 该函数在组件属性被赋值后立即执行,早于所有生命周期钩子函数,只运行一次
5 // props 是一个对象,包含了所有的组件属性值
6 // context 是一个对象,提供了组件所需的上下文信息
7 },
8};
context 对象的成员
| 成员 |
类型 |
说明 |
| attrs |
对象 |
同vue2的this.$attrs |
| slots |
对象 |
同vue2的this.$slots |
| emit |
方法 |
同vue2的this.$emit |
生命周期函数
| vue2 option api |
vue3 option api |
vue 3 composition api |
| beforeCreate |
beforeCreate |
不再需要,代码可直接置于 setup 中 |
| created |
created |
不再需要,代码可直接置于 setup 中 |
| beforeMount |
beforeMount |
onBeforeMount |
| mounted |
mounted |
onMounted |
| beforeUpdate |
beforeUpdate |
onBeforeUpdate |
| updated |
updated |
onUpdated |
| beforeDestroy |
改 beforeUnmount |
onBeforeUnmount |
| destroyed |
改 unmounted |
onUnmounted |
| errorCaptured |
errorCaptured |
onErrorCaptured |
| - |
新 renderTracked |
onRenderTracked |
| - |
新 renderTriggered |
onRenderTriggered |
beforeCreate 和 created 去除?在这两个后,已经有了数据响应式,而在 composition api 中,响应式已经被抽离出去(见上节)
新增钩子函数说明:
| 钩子函数 |
参数 |
执行时机 |
| renderTracked |
DebuggerEvent |
渲染 vdom 收集到的每一次依赖时 |
| renderTriggered |
DebuggerEvent |
某个依赖变化导致组件重新渲染时 |
DebuggerEvent:
- target: 跟踪或触发渲染的对象
- key: 跟踪或触发渲染的属性
- type: 跟踪或触发渲染的方式
TIP
composition api 相比于 option api 有哪些优势?
- 为了更好的逻辑复用(粒度更细了,组件间相同的功能逻辑复用)和代码组织
- 更好的类型推导(利于 TypeScript 类型推导)
有了 composition api,配合 reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。
相比于 option api,composition api 中没有了指向奇怪的 this,所有的 api 变得更加函数式,这有利于和类型推断系统比如 TS 深度配合。
https://vuejs.org/guide/built-ins/suspense.html
共享数据
vuex 方案
安装vuex@4.x
两个重要变动:
- 去掉了构造函数
Vuex,而使用createStore创建仓库
- 为了配合
composition api,新增useStore函数获得仓库对象
global state
由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,轻松制造多个全局响应式数据

1// store/useLoginUser 提供当前登录用户的共享数据
2// 以下代码仅供参考
3import { reactive, readonly } from "vue";
4import * as userServ from "../api/user"; // 导入api模块
5// 创建默认的全局单例响应式数据,仅供该模块内部使用
6const state = reactive({ user: null, loading: false });
7// 对外暴露的数据是只读的,不能直接修改
8// 也可以进一步使用toRefs进行封装,从而避免解构或展开后响应式丢失
9export const loginUserStore = readonly(state);
10
11// 登录
12export async function login(loginId, loginPwd) {
13 state.loading = true;
14 const user = await userServ.login(loginId, loginPwd);
15 state.loginUser = user;
16 state.loading = false;
17}
18// 退出
19export async function loginOut() {
20 state.loading = true;
21 await userServ.loginOut();
22 state.loading = false;
23 state.loginUser = null;
24}
25// 恢复登录状态
26export async function whoAmI() {
27 state.loading = true;
28 const user = await userServ.whoAmI();
29 state.loading = false;
30 state.loginUser = user;
31}
Provide&Inject
在vue2中,提供了provide和inject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用

除了兼容vue2的配置式注入,vue3在composition api中添加了provide和inject方法,可以在setup函数中注入和使用数据

考虑到有些数据需要在整个 vue 应用中使用,vue3还在应用实例中加入了provide方法,用于提供整个应用的共享数据
1creaetApp(App).provide("foo", ref(1)).provide("bar", ref(2)).mount("#app");

因此,我们可以利用这一点,在整个 vue 应用中提供共享数据
1// store/useLoginUser 提供当前登录用户的共享数据
2// 以下代码仅供参考
3import { readonly, reactive, inject } from "vue";
4const key = Symbol(); // Provide的key
5
6// 在传入的vue应用实例中提供数据
7export function provideStore(app) {
8 // 创建默认的响应式数据
9 const state = reactive({ user: null, loading: false });
10 // 登录
11 async function login(loginId, loginPwd) {
12 state.loading = true;
13 const user = await userServ.login(loginId, loginPwd);
14 state.loginUser = user;
15 state.loading = false;
16 }
17 // 退出
18 async function loginOut() {
19 state.loading = true;
20 await userServ.loginOut();
21 state.loading = false;
22 state.loginUser = null;
23 }
24 // 恢复登录状态
25 async function whoAmI() {
26 state.loading = true;
27 const user = await userServ.whoAmI();
28 state.loading = false;
29 state.loginUser = user;
30 }
31 // 提供全局数据
32 app.provide(key, {
33 state: readonly(state), // 对外只读
34 login,
35 loginOut,
36 whoAmI,
37 });
38}
39
40export function useStore(defaultValue = null) {
41 return inject(key, defaultValue);
42}
43
44// store/index
45// 应用所有store
46import { provideStore as provideLoginUserStore } from "./useLoginUser";
47// 继续导入其他共享数据模块...
48// import { provideStore as provideNewsStore } from "./useNews"
49
50// 提供统一的数据注入接口
51export default function provideStore(app) {
52 provideLoginUserStore(app);
53 // 继续注入其他共享数据
54 // provideNewsStore(app);
55}
56
57// main.js
58import { createApp } from "vue";
59import provideStore from "./store";
60const app = createApp(App);
61provideStore(app);
62app.mount("#app");
对比
|
vuex |
global state |
Provide&Inject |
| 组件数据共享 |
✅ |
✅ |
✅ |
| 可否脱离组件 |
✅ |
✅ |
❌ 和 vue 应用/组件深入绑定 |
| 调试工具 |
✅ |
❌ |
✅ |
| 状态树 |
✅ |
自行决定 |
自行决定 |
| 量级 |
重 |
轻 |
轻 |
script setup
script setup的提案文档:https://github.com/vuejs/rfcs/blob/script-setup-2/active-rfcs/0000-script-setup.md
1<template>
2 <hello-world></hello-world>
3 <p>the number is {{ count }}</p>
4 <button @click="increase">click to increase</button>
5</template>
6
7<script>
8import HelloWorld from "./HelloWorld.vue";
9import { ref } from "vue";
10export default {
11 setup() {
12 const count = ref(0);
13 const increase = () => {
14 count.value++;
15 };
16 return {
17 HelloWorld,
18 count,
19 increase,
20 };
21 },
22};
23</script>
当我们使用composition api时,往往整个组件对象的配置中只有一个setup
因此,vue3在SFC中使用script setup来简化代码书写
1<template>
2 <hello-world></hello-world>
3 <p>the number is {{ count }}</p>
4 <button @click="increase">click to increase</button>
5</template>
6
7<script setup>
8import HelloWorld from "./HelloWorld.vue";
9import { ref } from "vue";
10const count = ref(0);
11const increase = () => {
12 count.value++;
13};
14</script>
在script setup中,所有的顶级绑定都会被使用setup的返回值,顶级绑定包括:
如何使用组件属性
1<template>
2 <h1>Hello {{ props.title }}</h1>
3</template>
4
5<script setup>
6import { defineProps } from "vue";
7const props = defineProps(["title"]);
8</script>
编译结果
1<template>
2 <h1>Hello {{ props.title }}</h1>
3</template>
4
5<script>
6export default {
7 props: ["title"],
8 setup() {},
9};
10</script>
如何使用组件事件
1<template>
2 <button @click="handleClick">Click Me</button>
3</template>
4
5<script setup>
6import { defineEmit } from "vue";
7const emit = defineEmit(["click"]);
8const handleClick = () => {
9 emit("click");
10};
11</script>
编译结果:
1<template>
2 <button @click="handleClick">Click Me</button>
3</template>
4
5<script>
6export default {
7 emits: ["click"],
8 setup(props, { emit }) {
9 const handleClick = () => {
10 emit("click");
11 };
12 return {
13 handleClick,
14 };
15 },
16};
17</script>
ref sugar
ref语法糖目前仍在提案阶段,具有一定的争议
ref sugar提案地址:https://github.com/vuejs/rfcs/blob/ref-sugar/active-rfcs/0000-ref-sugar.md
1<template>
2 <!-- 受模板上下文代理的影响,在模板中直接当做原始值使用 -->
3 <h1>Hello, {{ fullName }}</h1>
4 <p>设置姓名</p>
5 <p>
6 姓:
7 <input type="text" v-model="firstName" />
8 </p>
9 <p>
10 名:
11 <input type="text" v-model="lastName" />
12 </p>
13</template>
14
15<script>
16import { ref, computed } from "vue";
17export default {
18 setup() {
19 const firstName = ref("邓"); // firstName 是一个对象
20 const lastName = ref("旭明"); // lastName 是一个对象
21 // 在 setup 函数中,则必须把 ref 当做对象使用
22 const fullName = computed(() => firstName.value + lastName.value);
23 return {
24 firstName,
25 lastName,
26 fullName,
27 };
28 },
29};
30</script>
使用ref时,主要有以下心智负担:
- 模板和
setup中对待ref不一致
ref、computed同reactive带来的响应数据结构不一致
为解决以上问题,vue3在SFC中提出ref sugar方案,它是在script setup方案上进一步的演化
上述代码可以用ref sugar简化为
1<template>
2 <!-- 使用ref标记的数据直接使用即可 -->
3 <h1>Hello, {{ fullName }}</h1>
4 <p>设置姓名</p>
5 <p>
6 姓:
7 <input type="text" v-model="firstName" />
8 </p>
9 <p>
10 名:
11 <input type="text" v-model="lastName" />
12 </p>
13</template>
14
15<script setup>
16import { computed } from "vue";
17ref: firstName = "邓"; // 这是一个ref
18ref: lastName = "旭明"; // 这是一个ref
19// 使用ref标记的数据直接使用即可
20const fullName = computed(() => firstName + lastName);
21</script>
上述js部分代码会被编译为
1<script setup>
2import { computed, ref } from "vue";
3const firstName = "邓";
4const lastName = "旭明";
5const fullName = computed(() => firstName.value + lastName.value);
6</script>
争议
ref sugar带来了新的js语义,并且同TypeScript语义冲突
ref sugar仅在script setup中有效,出了SFC环境就会失效,心智负担不减反增
ref sugar虽然降低了初学者的使用成本,但却增加了理解成本
- 当使用
TypeScript的时候,可以从编辑器中获得智能提示,可以很轻松的辨别一个数据是否是ref,根本没必要解决这个问题,否则会让事情变得越来越复杂。
Suspence
UserName组件的setup函数是异步的,只有等到它的setup函数加载完成后才能够显示模板
1<template>
2 <h1>Hello {{ name }}</h1>
3</template>
4
5<script>
6function delay(duration = 1000) {
7 return new Promise((resolve) => {
8 setTimeout(() => {
9 resolve();
10 }, duration);
11 });
12}
13export default {
14 async setup() {
15 await delay();
16 return {
17 name: "邓哥",
18 };
19 },
20};
21</script>
App组件使用了UserName组件,并使用了Suspense组件等待UserName组件渲染完成。
1<template>
2 <Suspense>
3 <template #default>
4 <UserName />
5 </template>
6 <template #fallback>
7 <h1>Loading...</h1>
8 </template>
9 </Suspense>
10</template>
11
12<script>
13import UserName from "./UserName.vue";
14export default {
15 components: {
16 UserName,
17 },
18};
19</script>
TypeScript
vue3对TypeScript支持良好,无论使用vue-cli还是vite,都可以达到开箱即用的效果。
使用vite的时候,由于没有针对.vue文件的类型定义,需要手动定义,以避免编辑器报错
自定义渲染器

vue3有着清晰的模块结构,特别是它将dom操作进行了分离,使得vue3的核心模块与dom操作脱钩,这样一来,就非常便于我们替换成其他的渲染模式。
hook 范式
1<template>
2 <h1>count:{{ countRef }}</h1>
3 <p>
4 <button @click="decrease">decrease</button>
5 <button @click="increase">increase</button>
6 </p>
7</template>
8
9<script>
10import { ref } from "vue";
11
12function useCount() {
13 let countRef = ref(0); //具有响应式了
14 // console.log(countRef);
15 const increase = () => {
16 countRef.value++;
17 };
18 const decrease = () => {
19 countRef.value--;
20 };
21 return {
22 countRef,
23 increase,
24 decrease,
25 };
26}
27
28export default {
29 setup() {
30 // console.log("所有生命周期钩子函数之前自动调用");
31 // console.log(this); // this -> undefined
32
33 // setup中,count是一个对象
34 // 实例代理中,count是一个count.value
35
36 //1. 新增
37
38 //2. 修改
39
40 //3. 删除
41 return {
42 ...useCount(),
43 };
44 },
45};
46</script>
说一下 vue3.0 是如何变得更快的?
优化 Diff 算法
相比 Vue 2,Vue 3 采用了更加优化的渲染策略。去掉不必要的虚拟 DOM 树遍历和属性比较,因为这在更新期间往往会产生最大的性能开销。
这里有三个主要的优化:
在没有动态改变节点结构的模板指令(例如 v-if 和 v-for)的情况下,节点结构保持完全静态。
当更新节点时,不再需要递归遍历 DOM 树。所有的动态绑定部分将在一个平面数组中跟踪。这种优化通过将需要执行的树遍历量减少一个数量级来规避虚拟 DOM 的大部分开销。
- 其次,编译器积极地检测模板中的静态节点、子树甚至数据对象,并在生成的代码中将它们提升到渲染函数之外。这样可以避免在每次渲染时重新创建这些对象,从而大大提高内存使用率并减少垃圾回收的频率。
- 第三,在元素级别。
编译器还根据需要执行的更新类型,为每个具有动态绑定的元素生成一个优化标志。
例如,具有动态类绑定和许多静态属性的元素将收到一个标志,提示只需要进行类检查。运行时将获取这些提示并采用专用的快速路径。
综合起来,这些技术大大改进了渲染更新基准,Vue 3.0 有时占用的 CPU 时间不到 Vue 2 的十分之一。
体积变小
重写后的 Vue 支持了 tree-shaking,像修剪树叶一样把不需要的东西给修剪掉,使 Vue 3.0 的体积更小。
需要的模块才会打入到包里,优化后的 Vue 3.0 的打包体积只有原来的一半(13kb)。哪怕把所有的功能都引入进来也只有 23kb,依然比 Vue 2.x 更小。像 keep-alive、transition 甚至 v-for 等功能都可以按需引入。
并且 Vue 3.0 优化了打包方法,使得打包后的 bundle 的体积也更小。
官方所给出的一份惊艳的数据:打包大小减少 41%,初次渲染快 55%,更新快 133%,内存使用减少 54%。
说一说相比 vue3.x 对比 vue2.x 变化
- 源码组织方式变化:使用 TS 重写
- 支持 Composition API:基于函数的 API,更加灵活组织组件逻辑(vue2 用的是 options api)
- 响应式系统提升:Vue3 中响应式数据原理改成 proxy,可监听动态新增删除属性,以及数组变化
- 编译优化:vue2 通过标记静态根节点优化 diff,Vue3 标记和提升所有静态根节点,diff 的时候只需要对比动态节点内容
- 打包体积优化:移除了一些不常用的 api(inline-template、filter)
- 生命周期的变化:使用 setup 代替了之前的 beforeCreate 和 created
- Vue3 的 template 模板支持多个根标签
- Vuex 状态管理:创建实例的方式改变,Vue2 为 new Store , Vue3 为 createStore
- Route 获取页面实例与路由信息:vue2 通过 this 获取 router 实例,vue3 通过使用 getCurrentInstance/ userRoute 和 userRouter 方法获取当前组件实例
- Props 的使用变化:vue2 通过 this 获取 props 里面的内容,vue3 直接通过 props
- 父子组件传值:vue3 在向父组件传回数据时,如使用的自定义名称,如 backData,则需要在 emits 中定义一下