人人都会AI编程

Vue 3 Diff 优化:静态提升、PatchFlags、Block 树

更新时间:2026-07-11

Vue 2 的虚拟 DOM Diff 采用的是双端对比算法,对整棵 VNode 树做递归的同层比较。虽然算法已经相当高效,但仍然存在一个本质问题:编译器把模板转换成渲染函数后,丢失了“哪些节点是静态的”这类信息,导致运行时不得不对每个节点进行逐属性对比,即使它永远不会改变。

Vue 3 的编译器则做了一件关键的事:在编译阶段就分析模板,把静态和动态信息记录下来,告诉运行时“哪些部分应该跳过,哪些部分只需比对特定属性”。这背后主要依赖三项优化手段:静态提升、PatchFlags 和 Block 树。

静态提升:不变的节点只创建一次

假设有这样一段模板:

<div>
  <span>我是静态标题</span>
  <p>{{ dynamicText }}</p>
</div>

Vue 2 的渲染函数在每次组件更新时,都会为 <span><p> 分别创建新的 VNode 对象。哪怕 <span> 里只有纯文本且永远不会改变,它仍然参与了完整的创建与对比流程。

Vue 3 的编译器会识别出 <span> 节点为静态节点,并将其“提升”到渲染函数的外部作用域:

// 编译后的伪代码
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "我是静态标题")

function render() {
  return _createElementVNode("div", null, [
    _hoisted_1,
    _createElementVNode("p", null, _toDisplayString(dynamicText.value))
  ])
}
  • _hoisted_1 只在模块初始化时创建一次,后续所有渲染都直接复用同一个 VNode 引用。
  • 在 Diff 阶段,当遇到相同引用的静态 VNode 时,Vue 可以直接跳过对比,因为它的内容不可能变化。
  • 如果模板中存在大量的静态骨架(比如表单布局、图文卡片),这项优化可以显著减少 VNode 创建的内存开销和 Diff 遍历的时间。

PatchFlags:标记动态节点该比什么

对于非静态节点,Vue 3 会为它们打上一个数字标记——PatchFlag,用以标识这个节点中哪些部分是需要运行时动态检查的。常见的标记包括:

| 标记值 | 含义 |
|--------|------|
| 1 | 文本内容动态({{ expression }}) |
| 2 | class 动态绑定 |
| 4 | style 动态绑定 |
| 8 | props 动态绑定 |
| 16 | 带 key 的 fragments |
| 组合值 | 例如 3 表示文本 + class 都是动态的 |

例如模板:

<div :class="cls" :style="stl">{{ text }}</div>

对应的 VNode 会被打上 PatchFlag = 3(即 TEXT + CLASS)。

在 Diff 时,Vue 根本不需要逐属性检查这个 VNode 的所有属性是否变化,而是只看 PatchFlag 对应的那些属性。比如 PatchFlag = 1 只比较文本内容;PatchFlag = 3 就只比较文本和 class。这样一来,大量的属性对比被直接跳过,更新过程变得极为轻量。

Block 树:跳过整棵静态子树的遍历

静态提升解决的是单个静态节点的问题,但真实模板中往往嵌套多层,比如一个包含静态头和静态列表项的 <ul>。如果按照传统递归 Diff,即使静态节点本身被提升,Vue 仍然需要遍历每一层去检查其子节点。

Vue 3 引入了 Block 树的概念来根治这个问题:在编译时,将模板中所有动态子节点收集到一个扁平数组中,该数组作为 VNode 的一个属性 dynamicChildren。而根节点成为一个“Block”,它内部可能包含其他 Block 或普通节点,但 Block 只负责对比自己的 dynamicChildren 数组,完全不再递归进入静态子节点。

示意图如下:

HTML 结构:
<div>                    ← Block Root
  <span>静态标题</span>
  <p>{{ msg }}</p>       ← 动态节点
  <ul>
    <li v-for="item in list" :key="item.id">
      {{ item.name }}   ← 动态节点
    </li>
  </ul>
</div>

编译后 Block 记录:
dynamicChildren = [p(VNode), li(VNode), li(VNode), ...]

在更新阶段,Vue 直接遍历 dynamicChildren,逐个对这些动态节点执行 Patch,而整棵静态子树(如 <span> 和那些纯静态的容器 <ul> 本身)根本不会出现在遍历路径中。这实际上实现了 树结构 flattened,靶向更新 的效果。

这三者如何协同工作?

以一个实际场景为例——一个后台管理列表页面,模板中大量是表结构、标签、图标等静态布局,只有数据行是动态的。编译后:

  1. 所有静态标题、表头都会被提升为常量 VNode,复用到底。
  2. 数据行对应的 <tr> 或单元格带有 PatchFlag,Diff 时只对比具体的文本和 class。
  3. 整个列表容器作为一个 Block,它的 dynamicChildren 数组中只包含那些数据行,父级的静态装饰部分完全被忽略。

这样一次数据更新,Vue 3 的实际对比节点数量可能只有 Vue 2 的几十分之一。这也是为什么 Vue 3 在大型模板场景下的性能提升能够达到倍数级,而不是小幅改良。

对开发者的实际影响

  • 你不需要刻意做任何事就能受益。这些优化全部在编译器层面自动完成,只要使用 template 编写组件,Vue 就会自动生成最优的更新策略。这也是官方推荐使用模板而不是手写 render 函数的重要原因之一:手写 render 可能会破坏 Block 树的优化结构。
  • key 属性依然重要。在 v-for 中正确使用 key 能帮助 Block 树精确追踪列表项,避免不必要的移动和重新创建。
  • 如果某些节点确定永远不变,可以使用 v-once 指令显式标记,强制将其视为静态内容,连动态追踪都省略。而 v-memo 则允许你手动指定依赖,控制子树是否跳过更新。

简单来说,Vue 3 的 Diff 优化让框架“更聪明地知道哪些东西不需要看”,从而把运行时的计算开销降到最低。这套机制在绝大多数真实应用中都是透明且有效的,你只管写好模板,剩下的交给编译器。