一、什么是Virtual(虚拟)DOM?
VirtualDOM 是对DOM的抽象,本质上它是个JavaScript对象,用一句话来简单理解,DOM上的内容较繁杂,这个对象就是更加轻量级的对DOM的描述。通过JavaScript对象表示DOM信息和结构,更新后使之与真实dom保持同步,同步的过程就是协调,核心是 diff算法,不太了解diff算法的同学可以检索相关文章进行学习。
二、Vue 为什么采用Virtual DOM?
- 创建真实DOM的代价高: 真实的DOM节点node实现的属性很多,而vnod 仅仅实现一些必要的属性,相比起来,创建一个vnode 的成本比较低。
- 触发多次浏览器重绘及回流: 使用vnode,相当于加了一个缓冲,让一次数据变动所带来的所有node变化,先在vnode中进行修改,然后diff之后对所有产生差异的节点集中一次对
DOM Tree进行修改,以减少浏览器的重绘及回流。 - 虚拟dom由于本质是一个JS对象,因此天生具备跨平台的能力,可以实现在不同平台的准确显示。
- VirtualDOM在性能上的收益并不是最主要的,更重要的是它使得Vue具备了现代框架应有的高级特性。
三、Vue3.0对Virtual DOM的优化
Vdom凭借着出色的性能成为了目前主流的前端框架都会选择的渲染方案。再加上优秀的 diff算法 对它的一步步的优化,使框架的价值得到了极致的体现,成为了我们前端框架必不可少的方案。
Vue2.x中的Vdom已经相当出色了,性能非常优秀。但在Vue3中还是对Vdom进行了重写,使Vue3突破了Vdom的性能瓶颈,贼拉快!
1.初试牛刀:
我们在Vue Template Explorer 上做测试可以更直观的看到Vnode的样子。
- 当我们创建一个静态 dom 元素的时候:
<span>Hello World!</span>
- Vue3编译后的Vdom长这个样子的:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("span", null, "Hello World!"))
}
看似比较复杂,实际上 _createElementBlock 函数中才是我们创建的 dom,从它身上我们可以看出,我们创建了一个 span 元素,内容为 “Hello World!”。这就是 Vdom 最基础的形式,在这里我们并不会感觉到Vue3与Vue2有什么区别。
2.patch flag 优化静态树
- 我们现在换一下,当我们创建一个动态的dom元素时:
<span>Hello World!</span>
<span>Good Morning!</span>
<span>{{name}}</span>
- Vue3编译后的Vdom长这个样子的:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("span", null, "Hello World!"),
_createElementVNode("span", null, "Good Morning!"),
_createElementVNode("span", null, _toDisplayString(_ctx.name), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
// 如果是动态绑定的val在渲染dom的时候会在 _createElementVNode函数后面追加动态标记1
创建动态 dom 元素的时候,Vdom 除了模拟出dom基本信息之外,还加了一个:1 /* TEXT */,这个值1便是标记。
这个标记就叫做 patch flag(补丁标记)
patch flag 的强大之处在于,当你的 diff 算法走到 _createElementBlock 函数的时候,会忽略所有的静态节点,只对有标记的动态节点进行对比,而且在多层的嵌套下依然有效。Vue2.x 中,重复渲染时静态不变化的内容依旧会重建 Vdom,diff 时仍需对比。
尽管JavaScript做 Vdom 的对比已经非常的快,但是 patch flag 的出现还是让 Vue3 的 Vdom 的性能得到了很大的提升,尤其是在针对比较复杂的大组件时。经过测试的 upadte 性能提升1.3到2倍,ssr 提升2到3倍
3.patch flag 优化静态属性
1.静态绑定
- 当我们创建一个有属性的元素:
<span id="hello">{{msg}}</span>
- Vue3编译后的Vdom是这个样子的:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("span", { id: "hello" }, _toDisplayString(_ctx.msg), 1 /* TEXT */))
}
让我们观察它的 patch flag,发现并没有对id做特殊的标记。是因为dom元素的静态属性在渲染的时候就已经创建了,并且是不会变动的,在后面进行更新的时候,diff 算法是不会去管它的。
2.动态绑定
- 当我们创建一个属性是动态绑定的元素:
<span :id="hello">{{msg}}</span>
- Vue3编译后的Vdom是这个样子的:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("span", { id: _ctx.hello }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"]))
}
再观察它的 patch flag,会发现变成了 9 /* TEXT, PROPS */, 而且后边还多了一个数组 ["id"]
静态标记中注释的内容,很明显的告诉我们,这个元素不止TEXT变化,它的属性PROPS也会变化。后边的数组内容则是有可能变化的属性
原来,Vue3 在 Vdom 的更新时,只会关注它有变化的部分。这样的优化使 Vue3 既跳出了 Vdom 的性能瓶颈,又依然保留了可以手写 render function 的灵活性。相当于 Vue3 既有 React 的灵活性,又有基于模板的性能保证。————尤雨溪
附上静态标记值的一份注释,里面使用了位运算:
export const enum PatchFlags {
TEXT = 1,// 1 表示具有动态textContent的元素
CLASS = 1 << 1, // 2 表示有动态Class的元素
STYLE = 1 << 2, // 4 表示动态样式(静态如style="color: red",也会提升至动态)
PROPS = 1 << 3, // 8 表示具有非类/样式动态道具的元素。
FULL_PROPS = 1 << 4, // 16 表示带有动态键的道具的元素,与上面三种相斥
HYDRATE_EVENTS = 1 << 5, // 32 表示带有事件监听器的元素
STABLE_FRAGMENT = 1 << 6, // 64 表示其子顺序不变的片段(没懂)。
KEYED_FRAGMENT = 1 << 7, // 128 表示带有键控或部分键控子元素的片段。
UNKEYED_FRAGMENT = 1 << 8, // 256 表示带有无key绑定的片段
NEED_PATCH = 1 << 9, // 512 表示只需要非属性补丁的元素,例如ref或hooks
DYNAMIC_SLOTS = 1 << 10, // 1024 表示具有动态插槽的元素
DEV_ROOT_FRAGMENT = 1 << 11, // 2048 表示仅因为用户在模板的根级别放置注释而创建的片段。这是一个仅用于开发的标志,生产中会剥离注释
// 以下两个是特殊的标记
HOISTED = -1, // 表示已提升的静态vnode,更新时调过整个子树
BAIL = -2 // 指示差异算法应该退出优化模式
}
4.静态提升
前面说的是Vue3突破Vdom的性能瓶颈的方式是只关注变化的部分。具体更新时候是咋做的?
具体方法是:静态树的提升和静态属性的提升
- 来,我们创建一些元素
<h1>Hi!</h1>
<span>Hello World!</span>
<span id="hello">{{msg}}</span>
- 静态提升之后:
const _hoisted_1 = /*#__PURE__*/_createElementVNode("h1", null, "Hi!", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "Hello World!", -1 /* HOISTED */)
const _hoisted_3 = { id: "hello" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1,
_hoisted_2,
_createElementVNode("span", _hoisted_3, _toDisplayString(_ctx.msg), 1 /* TEXT */)
], 64 /* STABLE_FRAGMENT */))
}
我们已经知道处理后的 Vdom 都在 _createElementBlock 函数之中,而观察发现,所有的静态元素都被放在了 _createElementBlock 函数之外了,也就是说他们只会在页面初始的时候被渲染一次,而在更新的时候,静态元素是不予搭理的。
这个优化就是 Vue3 的 静态提升能力。
5. 事件缓存
了解React的同学知道,使用 React 时,性能优化的其中一点就是将事件侦听方法手动进行缓存,避免更新组件时重复创建。而 Vue3 直接替我们做了这一步。
- 创建一个带事件的元素
<div id="app">
<button @click="handleBtnClick">纳尼</button>
</div>
- 编译后的Vdom:
const _hoisted_1 = { id: "app" }
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", _hoisted_1, [
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleBtnClick && _ctx.handleBtnClick(...args)))
}, "纳尼")
]))
}
传入的事件的储存位置变成了缓存的形式。当你的页面在不断的更新的时候,你的事件侦听器并不会重复地销毁再创建,而是以缓存的形式存在避免了重复渲染。秀哇,Vue3 在性能方面又有了一个出彩的地方。
6. 总结
- Vue3在性能优化细节上的方方面面,拿捏的明明白白,直接在编译的时候给你做到最好,这是它最有价值的地方。相当的妙啊~~
- 对于
diff算法的优化,Vue和React的区别在于,Vue控制diff算法需要计算的内容多少,React选择恰当的执行算法时机。 - 怎么说呢,相互借鉴,取其精华去其糟粕,毕竟小孩子才做选择,成年人都是all in。