Skip to content

L34 · 编译器优化:静态缓存与 PatchFlag ​

🎯 本节目标:理解 Vue 3 模板编译器的优化策略——让 Diff 更快
📦 本节产出:理解 PatchFlag、静态缓存/提升与 Block Tree
🔗 前置钩子:L33 的虚拟 DOM 和 Diff 算法
🔗 后续钩子:L35 将讲调度器与批量更新

1. 编译 vs 运行时 ​

Vue 3 的优势:在编译时就知道模板结构,把运行时能省的工作提前做掉。

这不意味着 JSX 无法静态分析。React Compiler 也能在满足其规则的代码上自动做记忆化优化;两者的编译目标和运行时机制不同,不能由语法形式直接判定性能胜负。React Compiler

本节固定 @vue/compiler-dom 3.5.43,以 { mode: 'module', hoistStatic: true } 编译纯模板。SFC 的 script setup 绑定信息、开发/生产模式等会改变具体输出,后文给出可复现脚本。


2. PatchFlag:精准知道哪里变了 ​

2.1 问题 ​

vue
<template>
  <div class="card">
    <h3>固定标题</h3>
    <p class="desc">这段文字永远不变</p>
    <span>{{ dynamicText }}</span>           <!-- 只有这里会变 -->
    <button :class="btnClass">确定</button>  <!-- 只有 class 会变 -->
  </div>
</template>

通用渲染器需要在运行时判断可能变化的内容。模板编译器能提前识别其中一些静态结构、动态文本与绑定,为运行时提供更具体的提示。

2.2 PatchFlag 标记 ​

以下使用第 5 节 flags 的紧凑模板(不含演示注释)生成,编译后的 render 函数给动态节点加上 flag:

javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "card" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _cache[0] || (_cache[0] = _createElementVNode("h3", null, "固定标题", -1 /* CACHED */)),
    _cache[1] || (_cache[1] = _createElementVNode("p", { class: "desc" }, "这段文字永远不变", -1 /* CACHED */)),
    _createElementVNode("span", null, _toDisplayString(_ctx.dynamicText), 1 /* TEXT */),
    _createElementVNode("button", {
      class: _normalizeClass(_ctx.btnClass)
    }, "确定", 2 /* CLASS */)
  ]))
}

这里 _ctx 与 _cache 都是 render 参数。TEXT 表明文本需检查,CLASS 表明 class 需检查;不能把“没有正数 patchFlag”一概理解为静态且永远跳过。手写 h() 也可能没有这些编译提示,运行时会采用通用路径。

2.3 PatchFlag 枚举 ​

以下为 3.5.43 的标记,通常作为优化路径中的提示,不是独立决定所有更新行为的开关:

Flag值含义
TEXT1动态文本
CLASS2元素的动态 class
STYLE4动态 style
PROPS8检查 dynamicProps 列出的绑定属性
FULL_PROPS16属性名集合可能变化,需要完整比较 props;不是 VNode 的 key
NEED_HYDRATION32需要属性水合,如部分事件/prop 绑定
STABLE_FRAGMENT64子节点顺序稳定的 Fragment
KEYED_FRAGMENT128有 key 或部分有 key 的子节点
UNKEYED_FRAGMENT256无 key 子节点
NEED_PATCH512ref、指令等非普通 props 更新需要纳入 patch
DYNAMIC_SLOTS1024带动态插槽的组件需要更新
DEV_ROOT_FRAGMENT2048开发模式根注释产生的 Fragment
CACHED-1缓存静态 VNode 的特殊标记
BAIL-2退出当前优化模式,采用更完整的比较

正数标记可按位组合,例如 TEXT | CLASS 为 3;负数特殊标记用相等判断,不能把 -1 当作“所有正数位都启用”。3.4 的 HOISTED 名称不要直接套进这份 3.5 枚举。3.5.43 patchFlags.ts


3. 静态缓存与常量提升 ​

3.1 问题 ​

如果每次 render 都重新创建相同静态 VNode,就会产生可避免的分配。编译器可以把这部分工作移出重复渲染路径,但不同版本不一定生成同一种代码。

3.2 优化 ​

下面这个模板由本节固定编译器生成的结果如下:

vue
<div class="card"><h1>静态标题</h1><p>{{ message }}</p><span>固定内容</span></div>
javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "card" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _cache[0] || (_cache[0] = _createElementVNode("h1", null, "静态标题", -1 /* CACHED */)),
    _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */),
    _cache[1] || (_cache[1] = _createElementVNode("span", null, "固定内容", -1 /* CACHED */))
  ]))
}

h1 和 span 首次渲染时写入当前组件实例的 _cache,后续复用。不能把它说成整个应用初始化时创建、所有实例共用唯一 VNode;静态 props 对象 _hoisted_1 则仍在 render 外部。

Vue 3.4.38 的一些输出会把静态 VNode 提到 render 外并标 HOISTED;3.5.43 的 cacheStatic 使用渲染缓存并标 CACHED。静态 props 等常量仍可提升,较大的连续静态内容也可能合为静态 HTML 字符串。确切输出取决于版本、模板与编译选项。3.5 cacheStatic、3.4 hoistStatic

3.3 静态属性提升 ​

上例的 div 包含动态后代,因此整个节点不能当作静态节点缓存,但 { class: "card" } 是常量,仍可提升。_hoisted_* 的名字本身不能说明“它一定是一个静态 VNode”,应查看被提升的实际值。


4. Block Tree:跳过整棵静态子树 ​

4.1 需要检查哪些节点 ​

<div>                          ← Diff ✓
  <header>                     ← Diff ✓
    <nav>                      ← Diff ✓
      <a>首页</a>              ← 结构静态
      <a>关于</a>              ← 结构静态
    </nav>
  </header>
  <main>                       ← Diff ✓
    <p>{{ message }}</p>       ← 文本动态
  </main>
</div>

这棵结构共有 7 个元素节点,其中 p 的文本依赖 message。通用遍历与编译辅助路径的工作范围不同;这里比较的是节点检查范围,不是一次真实性能测量。

4.2 Block + dynamicChildren ​

编译器生成 openBlock / createElementBlock 调用,运行时在创建 VNode 时收集当前 block 的动态后代。对这个固定结构,实际输出为:

javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", null, [
    _cache[0] || (_cache[0] = _createElementVNode("header", null, [
      _createElementVNode("nav", null, [
        _createElementVNode("a", null, "首页"),
        _createElementVNode("a", null, "关于")
      ])
    ], -1 /* CACHED */)),
    _createElementVNode("main", null, [
      _createElementVNode("p", null, _toDisplayString(_ctx.message), 1 /* TEXT */)
    ])
  ]))
}

在这个例子里,根 block 的 dynamicChildren 包含 p。进入匹配的优化更新路径后,可以直接检查它,而不必逐层搜索静态后代:

遇到 v-if / v-for 等结构变化时,编译器会建立相应 block 边界,运行时仍要处理分支切换、列表 diff、组件和 props;退出优化时也会回到通用路径。不能把 Block Tree 概括为“整个应用只访问一个动态数组”。


5. 用 Vue SFC Playground 观察编译结果 ​

可以在 Vue SFC Playground 选择对应 Vue 版本并查看 JS 编译结果。界面会变化,不能把某个按钮位置当作固定契约。

要精确复现本文的纯模板输出,在 L31 实验目录安装相同版本的编译器:

bash
npm install -D @vue/compiler-dom@3.5.43
typescript
// labs/mini-reactivity/compile-demo.ts
import { compile } from '@vue/compiler-dom'
const samples = {
  flags: '<div class="card"><h3>固定标题</h3><p class="desc">这段文字永远不变</p><span>{{ dynamicText }}</span><button :class="btnClass">确定</button></div>',
  statics: '<div class="card"><h1>静态标题</h1><p>{{ message }}</p><span>固定内容</span></div>',
  block: '<div><header><nav><a>首页</a><a>关于</a></nav></header><main><p>{{ message }}</p></main></div>',
}
for (const [name, template] of Object.entries(samples)) {
  console.log(name)
  console.log(compile(template, { mode: 'module', hoistStatic: true }).code)
}

运行 npx tsx compile-demo.ts。核对 _cache、CACHED、静态 props 的 _hoisted_1、TEXT/CLASS 与 block helper;修改模板后再次观察,而不是背变量编号。若改用 SFC script setup,绑定可能显示为 $setup 等形式,这是编译输入信息不同。


6. 本节总结 ​

检查清单 ​

  • [ ] 理解 PatchFlag 的作用(精确标记哪些属性是动态的)
  • [ ] 能列举常见 PatchFlag(TEXT / CLASS / STYLE / PROPS)
  • [ ] 能区分 3.5 的实例静态缓存、常量提升与较旧的 VNode 提升
  • [ ] 理解 Block Tree 的动态后代与优化路径边界
  • [ ] 知道在 Vue SFC Playground 中观察编译输出
  • [ ] 不把模板/JSX 语法直接当作性能结论,能说明需要什么测量

Git 提交 ​

bash
git add .
git commit -m "L34: PatchFlag + 静态提升 + Block Tree"

🔬 深度专题 ​

📖 D04 · 编译时优化:静态提升 + 靶向更新 — 编译信息怎样减少运行时检查?

🔗 → 下一节 ​

L35 将讲解调度器原理——为什么同一轮同步修改通常会合并组件更新,以及 nextTick 等待的是哪一次刷新。