D04 · 编译优化:PatchFlag 与 Block Tree
对应主课: L34 编译器优化 最后核对: 2026-09-23 与 L34 的关系: L34 侧重"看懂编译输出",本篇侧重"为什么这样编译"和"动手验证"
1. Vue 3 编译器三阶段
本篇固定 @vue/compiler-dom 3.5.43。模板编译可分为 parse、transform、generate;SFC 还包含脚本、样式与绑定元数据的处理,不能把模板编译等同于整个 .vue 文件的构建:
<template>
<div class="card">
<h3>标题</h3> <!-- 静态 -->
<p>{{ description }}</p> <!-- 动态文本 -->
<span :class="statusClass">ok</span> <!-- 动态 class -->
</div>
</template>对去掉空白和注释后的同一模板,使用 { mode: 'module', hoistStatic: true, cacheHandlers: true } 得到:
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 */)),
_createElementVNode("p", null, _toDisplayString(_ctx.description), 1 /* TEXT */),
_createElementVNode("span", {
class: _normalizeClass(_ctx.statusClass)
}, "ok", 2 /* CLASS */)
]))
}静态 h3 位于当前实例的渲染缓存,静态 props 对象位于 render 外;这两者都减少重复分配,但生命周期与共享范围不同。§5 的本地脚本可重现输出。
2. PatchFlag 全量表
以下是 Vue 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
位运算组合
PatchFlag 使用位运算组合多种动态性:
// 同时有动态文本 + 动态 class
// TEXT(1) | CLASS(2) = 3
createVNode("p", { class: cls }, text, 3 /* TEXT | CLASS */)
// 正数动态标记才按位判断;CACHED/BAIL 等特殊值另作相等判断。
if (patchFlag > 0) {
if (patchFlag & 1) { /* 检查文本 */ }
if (patchFlag & 2) { /* 检查 class */ }
}
// 互不影响,一次位运算就能判断3. 静态缓存、常量提升与事件缓存
§1 的 h3 第一次 render 时创建,以后从实例的 _cache[0] 复用。两个组件实例各有自己的 renderCache,不能在模块顶层手写一个 VNode 后,假定它可零成本在所有实例和位置复用。Vue 3.4 的 HOISTED 示例属于另一版输出,3.5 的静态 props 等常量仍可提升。3.5 cacheStatic
事件处理器缓存又是另一种优化。对 <button @click="handleClick">Click</button> 启用 cacheHandlers 后,实际生成:
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("button", {
onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
}, "Click"))
}缓存的是当前实例的包装函数;调用时仍会读取当时的 _ctx.handleClick。这不要求业务处理器“纯静态、不能读取动态状态”,也不是把某个实例的方法提升成模块共享函数。循环变量、绑定元数据与编译选项还会影响能否缓存。vOn 编译转换
4. Block Tree
编译器生成 openBlock/createElementBlock 等调用,运行时据此收集当前 block 的动态后代或子 block 到 dynamicChildren。结构对应稳定时,patch 可以直接处理这组节点,跳过静态子树:
页面有 100 个节点,3 个是动态的:
假设一个结构稳定 block 只有 3 个需检查的动态后代:
优化路径可处理这 3 个 dynamicChildren,跳过其间的静态层级。
这说明减少的节点检查,不是整页快了 100 / 3 倍。Block 的结构不稳定问题
条件与列表可能改变结构。编译器用 branch/Fragment 等 block 边界表达变化,列表仍需按有无 key 等情况协调;并不是给所有 v-for 情况统一套一个不变数组:
5. 动手实验:Vue Playground 验证
实验 1:观察 PatchFlag
- 打开 Vue SFC Playground
- 把 Vue 版本切换到 3.5.43,再查看编译后的 JavaScript 面板;界面位置可能变化
- 输入以下模板,观察编译输出中的数字注释:
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
const cls = ref('active')
</script>
<template>
<div>
<h1>静态标题</h1>
<p>{{ msg }}</p>
<span :class="cls">文字</span>
<a :href="'/page/' + msg" :class="cls">链接</a>
</div>
</template>预期观察:
h1→CACHED (-1),首次写入当前实例渲染缓存,以后复用p→TEXT (1),只有文本是动态的span→CLASS (2),只有 class 是动态的a→PROPS|CLASS (10),即8 + 2,href 和 class 都是动态的
实验 2:固定版本,检查生成结构
继续使用 L31–L37 的 labs/mini-reactivity,Vue 与 compiler-dom 都固定 3.5.43(编译器已在 L34 安装)。保存 compile-check.mjs,运行 node compile-check.mjs。这段只在本地对已知模板执行生成函数,不把外部输入交给 new Function:
// labs/mini-reactivity/compile-check.mjs
import assert from 'node:assert/strict'
import * as Vue from 'vue'
import { compile } from '@vue/compiler-dom'
const card = '<div class="card"><h3>标题</h3><p>{{ description }}</p><span :class="statusClass">ok</span></div>'
const flags = '<div><h1>静态标题</h1><p>{{ msg }}</p><span :class="cls">文字</span><a :href="\'/page/\' + msg" :class="cls">链接</a></div>'
const event = '<button @click="handleClick">Click</button>'
for (const template of [card, flags, event]) {
console.log(compile(template, { mode: 'module', hoistStatic: true, cacheHandlers: true }).code)
}
function makeRender(template) {
const { code } = compile(template, {
mode: 'function', prefixIdentifiers: true, hoistStatic: true, cacheHandlers: true,
})
return new Function('Vue', code)(Vue)
}
const render = makeRender(flags)
const cache = []
const first = render({ msg: 'hello', cls: 'active' }, cache)
const second = render({ msg: 'world', cls: 'active' }, cache)
assert.equal(first.children[0], second.children[0])
assert.equal(first.children[0].patchFlag, -1)
assert.deepEqual(first.dynamicChildren.map(node => node.patchFlag), [1, 2, 10])
assert.equal(second.children[1].children, 'world')
const anotherInstance = render({ msg: 'other', cls: 'active' }, [])
assert.notEqual(first.children[0], anotherInstance.children[0])
const renderEvent = makeRender(event)
const eventCache = []
let called = ''
const context = { handleClick: () => { called = 'first' } }
const button1 = renderEvent(context, eventCache)
context.handleClick = () => { called = 'second' }
const button2 = renderEvent(context, eventCache)
assert.equal(button1.props.onClick, button2.props.onClick)
button2.props.onClick()
assert.equal(called, 'second')
console.log('缓存、动态标记与事件包装函数检查通过')这里检查的是生成的 VNode/缓存结构,没有实际挂载 DOM,也没有测量耗时;实际组件挂载验证 DOM 行为,L30 的测量流程则用于评估性能。
6. 性能实际影响
| 优化 | 减少的工作 | 仍然存在的工作 |
|---|---|---|
| 动态标记 | 避免逐个检查已知不会变化的属性 | 动态表达式求值、相应 DOM 操作 |
| 静态缓存 | 避免反复分配同一实例的静态 VNode | 首次创建、缓存保存、实际 DOM 生命周期 |
| Block | 跳过稳定结构中的静态子树检查 | block 边界、列表协调、组件更新与回退路径 |
不能把比较次数直接换算成速度提升。原生 DOM、布局绘制、组件状态、数据获取与设备差异都会影响实测结果;空循环或一边创建对象一边只比较常量的计时,既不等价,也可能被引擎优化掉。按 L30 固定场景测量,记录优化前后条件与结果,才有依据报告倍数。
7. 总结
- PatchFlag 让 diff 精确到属性级别,用位运算组合多种动态性
- 3.5 静态 VNode 缓存与常量提升分属不同机制,事件包装函数也可按实例缓存
- 稳定 block 可只检查动态后代;这不是整个应用或 keyed diff 的统一复杂度结论
- 条件与列表有相应结构边界,仍需协调增删与移动
- 编译器提供提示,运行时利用提示执行优化;手写 h、动态结构等可能使用不同路径
- 用固定版本本地脚本重现输出,再用浏览器与测量工具验证真实行为和性能