详解Vue3.0对虚拟Dom的优化

3,309 阅读6分钟

一、什么是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 中,重复渲染时静态不变化的内容依旧会重建 Vdomdiff 时仍需对比。

newold.png

尽管JavaScript做 Vdom 的对比已经非常的快,但是 patch flag 的出现还是让 Vue3Vdom 的性能得到了很大的提升,尤其是在针对比较复杂的大组件时。经过测试的 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。