Skip to main content

渲染模式

渲染策略演变

Vue 是一个响应式框架,数据变更会触发视图更新,因此高效的渲染机制对于提升应用性能至关重要,否则可能会影响页面的流畅度和用户体验。

  • 1.0:DOM-based Templating(基于DOM的模板)。
  • 2.0:Pure Virtual DOM(纯虚拟DOM)。
  • 3.0:Compiler-enhanced Virtual DOM(编译器增强型虚拟DOM)。
  • 3.6:Vapor Mode(蒸汽模式)。

纯虚拟DOM

React 是最早提出虚拟 DOM 概念的库,而 Vue 在 2.0 版本引入了 VDOM,其采用虚拟 DOM 的主要原因:

  • 对渲染过程进行抽象化,有助于处理复杂逻辑。
  • 有了单文件组件(SFC)方式,把颗粒度提升到了组件上。
  • 适配 DOM 以外的渲染目标。
/*
<div id="hello"></div>
*/
const vnode = {
type: 'div',
props: {
id: 'hello'
},
children: [
/* more vnodes */
]
}

增强型虚拟DOM

编译时优化

虚拟 DOM 的对比和渲染以及响应式数据的依赖收集,都是在运行时完成的,所以算法无法预知新的虚拟 DOM 树会是怎样,因此总是需要遍历整棵树,比较每个 VNODE 上 props 的区别来确保正确性。因此,即使一棵树的某个部分从未改变,但还是会在每次重渲染时创建新的 VNODE,带来了大量不必要的内存压力。

Vue 包含编译器和运行时两部分,在 Vue 3.0 中提出在编译器中做一些虚拟 DOM 的优化手段,从而提升渲染性能,这就是编译器增强型虚拟 DOM。

静态缓存

渲染器在首次渲染时会将创建的这部分 VNODE 缓存起来,并在后续的重新渲染中使用缓存的 VNODE,渲染器知道新旧 VNODE 在这部分是完全相同的,所以会完全跳过对它们的差异比对。

<script setup>
import { ref } from 'vue'
const baz = ref('baz')
</script>
<template>
<div>
<div>foo</div> <!-- 需缓存 -->
<div>bar</div> <!-- 需缓存 -->
<div>{{ baz }}</div>
</div>
</template>

编译后如下:

function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
// _cache 表示缓存
_cache[0] || (_cache[0] = _createElementVNode("div", null, "foo", -1 /* HOISTED */)),
_createCommentVNode(" 需缓存 "),
_cache[1] || (_cache[1] = _createElementVNode("div", null, "bar", -1 /* HOISTED */)),
_createCommentVNode(" 需缓存 "),
_createElementVNode("div", null, _toDisplayString($setup.baz), 1 /* TEXT */)
]))
}

当有足够多连续的静态元素时,它们会再被压缩为一个静态 VNODE,其中包含的是这些节点相应的纯 HTML 字符串。

<script setup>
import { ref } from 'vue';
const bar = ref('bar');
</script>
<template>
<div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div class="foo">foo</div>
<div>{{ bar }}</div>
</div>
</template>

编译后如下:

function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
// 压缩为一个并缓存
_cache[0] || (_cache[0] = _createStaticVNode("<div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div><div class=\"foo\">foo</div>", 5)),
_createElementVNode("div", null, _toDisplayString($setup.bar), 1 /* TEXT */)
]))
}
更新类型标记

更新类型标记,可以快速定位要更新的属性类型。

<script setup>
import { ref } from 'vue';
const foo = ref('foo');
const bar = ref('bar');
</script>
<template>
<div>
<div :class="foo"></div>
<div :id="bar"></div>
<div
:class="foo"
:id="bar"></div>
</div>
</template>

编译后如下:

function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_createElementVNode("div", {
class: _normalizeClass($setup.foo)
}, null, 2 /* CLASS */),
_createElementVNode("div", { id: $setup.bar }, null, 8 /* PROPS */, _hoisted_1),
_createElementVNode("div", {
class: _normalizeClass($setup.foo),
id: $setup.bar
}, null, 10 /* CLASS, PROPS */, _hoisted_2)
]))
}
树结构打平

打平树结构,节省 Diff 对比次数。

<script setup>
import { ref } from 'vue';
const foo = ref('foo');
const bar = ref('bar');
</script>
<template>
<div>
<div>...</div>
<div :id="foo"></div>
<div>
<div>{{ bar }}</div>
</div>
</div>
</template>

编译后如下:

function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createElementBlock("div", null, [
_cache[0] || (_cache[0] = _createElementVNode("div", null, "...", -1 /* HOISTED */)),
_createElementVNode("div", { id: $setup.foo }, null, 8 /* PROPS */, _hoisted_1),
// 没有任何的标记
_createElementVNode("div", null, [
_createElementVNode("div", null, _toDisplayString($setup.bar), 1 /* TEXT */)
])
]))
}

运行时优化

Vue 2.0 采用双端对比:

Vue 3.0 采用最长递增子序列算法,减少对比次数:

Signal

从根本上说,Signal 是与 Vue 中的 ref 相同的响应性基础类型,它是一个在访问时跟踪依赖、在变更时触发副作用的值容器。

随着 Signal 能力的增强,当前的响应式系统可以从组件级颗粒度回归到节点级颗粒度,而不会影响性能。

Vue 3.6 内置了 alien-signals 1.0 版本。alien-signals 对比 @vue/reactivify 的优势:施加了一些约束(例如不使用 Array/Set/Map 和不允许函数递归)来确保性能,在保持算法的简单性比复杂的调度策略提供更显著的改进。

Vapor Mode

在 Vue3.6 中在 alien-signals 的加持下,无虚拟 DOM 的方案得以实现。

Vapor Mode 的底层原理:

  1. 基于强大的响应式系统,最小颗粒化的收集依赖的所有副作用。
  2. 在对应的副作用中进行最小颗粒化的真实 DOM 的更新。
<script setup vapor>
import { ref } from 'vue';
const baz = ref('baz');
</script>

<template>
<div>foo</div>
<div>bar</div>
<div>{{ baz }}</div>
</template>

编译后为:

const t0 = _template("<div>foo</div>")
const t1 = _template("<div>bar</div>")
const t2 = _template("<div> </div>")
function render(_ctx, $props, $emit, $attrs, $slots) {
const n0 = t0()
const n1 = t1()
const n2 = t2()
const x2 = _child(n2)
_renderEffect(() => _setText(x2, _toDisplayString(_ctx.baz)))
return [n0, n1, n2]
}