【源码】vue3 为什么说渲染、更新更快、内存占用更少
【公司内部分享】探索Vue.js框架内部原理和实现机制,旨在理解其核心概念和技术细节,以便更好地应用和定制Vue.js。
相关文章
- vue3 为什么说渲染、更新更快、内存占用更少
- vue3 为什么说打包体积更小
- vue3 为什么说跨平台能力更强
- vue2 项目结构与架构设计介绍
- vue2 变化侦测与异步更新源码刨析
- vue2 模板编译源码刨析
- vue2 patch源码刨析
- vuex4 源码浅析
vue3 为什么说渲染、更新更快、内存占用更少
一、栈调度到协调更新
Vue 2 使用栈调度,是一种同步执行更新任务的方式。在栈调度中,更新任务会被依次放入执行栈中,并按照入栈顺序依次执行。只有当一个更新任务执行完毕后,才会执行下一个更新任务。
- 优点:栈调度的实现简单直观,易于理解和调试。
- 缺点:栈调度可能会导致长时间的任务阻塞主线程,影响页面的响应速度和流畅性。由于更新任务是同步执行的,因此无法有效地处理优先级较高的任务,可能会导致用户交互不及时。
Vue 3 使用协调更新,是一种基于优先级的更新处理方式。在协调更新中,更新任务会根据一定的优先级被调度执行,以确保优先级较高的任务能够得到更及时的响应。协调更新通常采用时间片段调度和任务优先级调度等策略,以提高页面的性能和用户体验。
优点:协调更新能够更灵活地处理更新任务,并根据任务的优先级来决定执行顺序,从而提高页面的响应速度和流畅性。它能够有效地处理优先级较高的任务,如用户交互和动画等。
缺点:协调更新的实现相对复杂,需要考虑更多的因素,如任务优先级、时间片段调度等。由于更新任务是异步执行的,可能会导致一些复杂的同步问题和调试困难。
关于Long Task详细可参考 怎样去优化Long Task
二、虚拟 DOM 的优化
Vue 3 中对虚拟 DOM 进行了优化,采用了更高效的算法来处理虚拟 DOM 的更新。在 Vue 3 中,虚拟 DOM 的 patching 过程更快,因为它采用了更精细的更新策略,减少了不必要的 DOM 操作。
整体渲染 vs 靶向更新
- 在 Vue 2 中,当数据发生变化时,整个组件树都会被重新渲染,即使只有部分节点发生了变化。这是因为 Vue 2 的 diff 策略是逐层比较,如果某一层有变化,整个子树都会被重新渲染。
1 |
|
- 在 Vue 3 中,通过通过 BlockTree 和 DynamicChild 实现了靶向更新机制,避免了对整个虚拟 DOM 树的全量比较,其中 BlockTree 记录组件的结构和状态,DynamicChild 则追踪动态子节点的信息,只有实际发生变化的节点会被更新,而不必重新渲染整个组件树。
1 |
|
精确度
Vue 2 的 diff 策略是基于逐层比较的,因此在某些情况下可能会导致不必要的渲染操作,即使节点的子树没有发生变化。
Vue 3 的靶向更新机制能够更精确地定位到需要更新的节点,避免了不必要的比较和渲染操作,从而提高了更新的精确度和效率。
三、静态节点的提取
组件实例 vs 全局
Vue 3 中通过静态节点的提取,将静态内容从动态内容中分离出来,这样可以避免不必要的重新渲染,提高了渲染的效率。
在 Vue 2 中,整个模板会被编译为 render 函数,并且所有的节点都被认为是动态的,因此每次数据更新时都会重新渲染整个模板。
1
with(this){return _c('div',[_c('h1',[_v(_s(title))]),_c('p',[_v("Static Content")]),_c('p',[_v(_s(dynamicContent))])])}
在这个示例中,
<p>Static Content</p>
虽然内容是静态的,但在渲染函数中仍然被视为动态节点,因此每次数据更新时都会重新渲染。在 Vue 3 中,编译器会识别静态节点并将其提取出来,在渲染函数中静态节点不会被重新渲染。
1
2
3
4
5
6
7const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "Static Content", -1 /* HOISTED */);
return (openBlock(), createBlock("div", null, [
createVNode("h1", null, toDisplayString(title), 1 /* TEXT */),
_hoisted_1,
createVNode("p", null, toDisplayString(dynamicContent), 1 /* TEXT */)
]));在这个示例中,
<p>Static Content</p>
被识别为静态节点,并被提取到了 _hoisted_1 常量中,在渲染函数中只会被创建一次,而不会被重新渲染。
在 Vue 3 中,不再使用 with 关键字来处理模板编译和渲染。相比之下,Vue 2 在模板编译时会使用 with 关键字来创建一个作用域,使得模板中的变量可以直接访问当前组件实例的数据和方法。然而,在 Vue 3 中,模板编译会将模板转换为 JavaScript 代码,并生成渲染函数。这些渲染函数会明确地使用 JavaScript 的变量引用,而不是依赖于 with 关键字来创建作用域。这样可以避免 with 带来的一些潜在问题和性能开销,并且更容易进行静态分析和优化。
四、事件侦听器的优化
Vue 3 中对事件侦听器进行了优化,采用了更快的事件绑定和解绑算法,提高了事件处理的效率。
事件委托机制
Vue 3 使用了事件委托机制来处理事件,即将事件监听器绑定到父元素上,通过事件冒泡的方式来触发事件处理函数。这样做的好处是可以减少事件监听器的数量,从而降低内存占用和提高事件处理的效率。
对于普通的事件绑定,例如 @click、@change 等,Vue 3 会将事件监听器绑定到组件的根元素上,然后利用事件冒泡机制来捕获和处理事件。这样可以减少事件监听器的数量,提高性能。而对于自定义事件(通过 this.$emit 触发的事件),事件监听器会绑定在当前组件实例上,而不是根元素上。
事件处理函数的缓存
Vue 3 在渲染过程中会对事件处理函数进行缓存,避免每次渲染时都重新创建事件处理函数。这样可以减少内存分配和回收的开销,提高了事件处理的效率。
编译阶段
在编译阶段,Vue 3 的编译器会遍历模板,识别事件绑定,并对事件处理函数进行静态分析。
1
2
3
4
5
6
7
8
9
10
11
12
13import { createRoot } from 'vue';
// 编译阶段处理模板
const template = `
<div>
<button @click="handleClick">Click me</button>
</div>
`;
const root = createRoot({
// 编译模板,静态分析事件处理函数
template
});事件处理函数缓存
在编译阶段,编译器会对事件处理函数进行缓存,将事件处理函数存储在一个内部的缓存对象中。
1
2
3
4
5
6
7
8// 定义事件处理函数
const handleClick = () => {
console.log('Button clicked!');
};
// 将事件处理函数缓存起来
const eventHandlersCache = new Map();
eventHandlersCache.set(handleClick, handleClick);渲染阶段
在渲染阶段,Vue 3 的运行时会根据编译阶段生成的渲染函数来渲染组件,并在渲染函数中访问事件处理函数的缓存。
1
2
3
4
5
6
7
8
9
10
11// 渲染函数中访问事件处理函数缓存
const render = () => {
// 根据模板生成渲染函数
// 在渲染函数中访问事件处理函数的缓存
const handleClick = eventHandlersCache.get(handleClick);
// 渲染组件
return `<button @click="${handleClick}">Click me</button>`;
};
// 执行渲染函数
root.render(render);
五、组件实例的优化
Proxy 替代 Object.defineProperty
Vue 3 中使用了 Proxy 来实现响应式数据的代理,而不再使用 Vue 2 中的 Object.defineProperty。Proxy 拥有更好的性能,并且在处理大量数据时占用的内存更少。
Object.defineProperty 是一个直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象的方法。它可以用来定义属性的 getter 和 setter 方法,以便在属性被访问或者修改时执行自定义的逻辑。
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// 创建一个空对象
const obj = {};
// 使用 Object.defineProperty 定义一个属性 'name'
Object.defineProperty(obj, 'name', {
// 定义 getter 方法,返回属性值
get() {
console.log('Getting name');
return this._name;
},
// 定义 setter 方法,设置属性值
set(value) {
console.log('Setting name to', value);
this._name = value;
},
// 可以配置属性的可枚举性、可配置性和可写性
enumerable: true,
configurable: true
});
// 访问属性 'name',会触发 getter 方法
console.log(obj.name); // 输出:Getting name
// 设置属性 'name',会触发 setter 方法
obj.name = 'Alice'; // 输出:Setting name to AliceProxy 对象用于创建一个对象的代理,从而可以拦截并对对象的各种操作进行自定义处理。它提供了一组可拦截的操作(例如访问属性、设置属性、删除属性等),以便在这些操作发生时执行自定义的逻辑。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23// 创建一个空对象
const target = {};
// 创建一个 Proxy 对象
const proxy = new Proxy(target, {
// 拦截属性的读取操作
get(target, key, receiver) {
console.log(`Getting ${key}`);
return target[key];
},
// 拦截属性的设置操作
set(target, key, value, receiver) {
console.log(`Setting ${key} to`, value);
target[key] = value;
return true;
}
});
// 访问属性,会触发 get 方法
console.log(proxy.name); // 输出:Getting name
// 设置属性,会触发 set 方法
proxy.name = 'Bob'; // 输出:Setting name to Bob
组件实例的精简设计
Vue 3 中对组件实例进行了精简设计,去除了一些不常用的属性和方法,从而减少了组件实例的内存占用。
减少不常用的内部属性和方法
例如一些用于实现响应式系统或者模板编译的内部属性和方法,在 Vue 3 中可能被重新设计或者优化,以减少内存占用。
优化内部数据结构
Vue 3 可能会对内部的数据结构进行优化,使其更加高效,并且减少不必要的内存占用。
去除不常用的配置项和生命周期钩子
Vue 3 可能会去除一些不常用的配置项和生命周期钩子,以减少组件实例的复杂度和内存占用。
虚拟 DOM 的优化
Vue 3 中对虚拟 DOM 进行了优化,减少了虚拟 DOM 对象的创建和销毁次数,从而减少了内存占用。此外,Vue 3 中还引入了 Fragment 和 Teleport 等新的虚拟 DOM 类型,进一步减少了虚拟 DOM 对象的内存占用。
Fragment
- 在 Vue 2 中,并没有直接的内置支持 Fragment 的方式,但你可以使用
<template>
标签来达到类似的效果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16<template>
<div>
<h1>Hello Vue 2</h1>
<template>
<p>This is the first paragraph.</p>
<p>This is the second paragraph.</p>
</template>
</div>
</template>
<script>
export default {
name: 'App',
}
</script>- 在 Vue 3 中,Fragment 提供了一种更加清晰、直观、语义化的方式来表示 Fragment,同时具有更好的兼容性和与 JSX 配合更方便的特点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21<template>
<fragment>
<h1>Title</h1>
<p>Paragraph 1</p>
<p>Paragraph 2</p>
</fragment>
</template>
<script>
import { defineComponent, h } from 'vue';
export default defineComponent({
render() {
return h(Fragment, [
h('h1', 'Title'),
h('p', 'Paragraph 1'),
h('p', 'Paragraph 2')
]);
}
});
</script>- 在 Vue 2 中,并没有直接的内置支持 Fragment 的方式,但你可以使用
Teleport
在 Vue 2 中,要实现类似 Teleport 的功能,你可能需要借助于一些第三方库或者手动操作 DOM。通常情况下,你可以使用 JavaScript 来操作 DOM 元素的位置,但这样做可能比较繁琐,并且不够灵活。
在 Vue 3 中,Teleport 允许我们在 DOM 中的任何地方渲染组件的内容,而不受父组件的限制。
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<template>
<teleport to="body">
<div class="modal">
<h2>Modal</h2>
<p>This modal is teleported to the body element</p>
</div>
</teleport>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
// ...
});
</script>
<style>
.modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: white;
padding: 20px;
border: 1px solid #ccc;
}
</style>
组件的异步加载和懒加载
Vue 3 支持组件的异步加载和懒加载,可以在需要时动态地加载组件,从而减少了页面初始化时的内存占用。
Vue 2 中的组件懒加载是通过 webpack 的动态 import 函数来实现的,webpack 会将异步加载的组件分割成单独的代码块,在需要时才动态加载。
1
Vue.component('async-component', () => import('./AsyncComponent.vue'));
Vue 3 中的组件懒加载是通过返回一个 Promise 来实现的,这个 Promise 在需要时会被解析,并且可以包装成一个工厂函数,返回一个组件定义对象。
1
2import { defineAsyncComponent } from 'vue';
const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));
Vue 3 中的组件异步加载和懒加载不再依赖 webpack 的 import 函数。Vue 3 提供了新的 API defineAsyncComponent 来实现组件的异步加载和懒加载,不再依赖于 webpack 的特定语法或功能。
六、Tree-Shaking 的支持
Options API 与 Composition API
Vue 2 主要使用的是 Options API,即通过一个选项对象来定义组件。这种方式使得组件的逻辑和模板耦合在一起,导致难以识别和移除未使用的代码。
Vue 3 引入了 Composition API,使得组件的逻辑可以更灵活地组织和重用,且可以更容易地识别和移除未使用的代码。Composition API 的代码可以更容易地进行 Tree-Shaking。