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 问题
<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:
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 | 值 | 含义 |
|---|---|---|
TEXT | 1 | 动态文本 |
CLASS | 2 | 元素的动态 class |
STYLE | 4 | 动态 style |
PROPS | 8 | 检查 dynamicProps 列出的绑定属性 |
FULL_PROPS | 16 | 属性名集合可能变化,需要完整比较 props;不是 VNode 的 key |
NEED_HYDRATION | 32 | 需要属性水合,如部分事件/prop 绑定 |
STABLE_FRAGMENT | 64 | 子节点顺序稳定的 Fragment |
KEYED_FRAGMENT | 128 | 有 key 或部分有 key 的子节点 |
UNKEYED_FRAGMENT | 256 | 无 key 子节点 |
NEED_PATCH | 512 | ref、指令等非普通 props 更新需要纳入 patch |
DYNAMIC_SLOTS | 1024 | 带动态插槽的组件需要更新 |
DEV_ROOT_FRAGMENT | 2048 | 开发模式根注释产生的 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 优化
下面这个模板由本节固定编译器生成的结果如下:
<div class="card"><h1>静态标题</h1><p>{{ message }}</p><span>固定内容</span></div>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 的动态后代。对这个固定结构,实际输出为:
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 实验目录安装相同版本的编译器:
npm install -D @vue/compiler-dom@3.5.43// 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 提交
git add .
git commit -m "L34: PatchFlag + 静态提升 + Block Tree"🔬 深度专题
📖 D04 · 编译时优化:静态提升 + 靶向更新 — 编译信息怎样减少运行时检查?
🔗 → 下一节
L35 将讲解调度器原理——为什么同一轮同步修改通常会合并组件更新,以及 nextTick 等待的是哪一次刷新。