03|编译器优化
第七章 / 03。目标:看懂编译器给渲染器留下的“哪里可能变化”证据。
Vue 为什么不是纯运行时 diff
模板语法受约束,编译器可以提前知道哪些内容永远静态、哪些只会改文本、class 或指定 props。Vue 因而采用 compiler-informed virtual DOM:运行时不必对每次更新都完整遍历并逐项猜测。
| 编译信息 | 编译阶段知道什么 | 更新阶段省去什么 |
|---|---|---|
| 静态缓存/提升 | 子树不读取运行时状态 | 重建 VNode 与 diff 静态子树 |
| Patch Flag | 节点只可能改变文本、class、style 或某些 props | 全量 props 比较 |
| Block Tree | 结构稳定区域中的动态后代有哪些 | 遍历整棵静态层级 |
| handler cache | 事件表达式可安全复用 | 因新函数身份造成的无谓更新 |
v-if、v-for 会改变结构,因此形成新的 block 边界。编译器优化不是“没有 VDOM”,而是给 VDOM patch 提供更窄的工作集。
最小可运行实验:检查编译产物
在 Vue 3.5.42 实验项目先运行 npm install -D --save-exact @vue/compiler-dom@3.5.42,显式声明直接使用的包,不依赖包管理器是否提升传递依赖。在项目根运行以下 compiler-lab.mjs;@vue/compiler-dom 是 Vue 模板编译器包:
import { compile } from '@vue/compiler-dom'
const template = `
<section>
<h2>固定标题</h2>
<p :class="{ hot }">{{ message }}</p>
</section>
`
const { code } = compile(template, {
mode: 'module',
hoistStatic: true,
cacheHandlers: true
})
console.log(code)
console.table({
hasCachedStatic: code.includes('CACHED') || code.includes('_hoisted_'),
marksDynamicText: code.includes('TEXT'),
marksDynamicClass: code.includes('CLASS')
})用 node compiler-lab.mjs 运行,再把 固定标题 改为 {{ title }}。当前编译器常把静态节点写进 render cache 并标为 CACHED,旧版本也可能生成 _hoisted_ 声明;第二版多了动态依赖,缓存关系会改变。生成代码的辅助函数名和数值 flag 属于当前编译器实现,不应在业务测试中逐字快照;学习实验关注“静态节点是否被缓存、动态节点被标了哪些变化类型”。
从标记到一次更新
例子中 p 的文本和 class 都依赖运行时,当前编译输出可能标为 3 /* TEXT, CLASS */:1表示文本,2表示class,按位或得到3。运行时用按位与判断要做哪些工作,不需要逐个扫描所有静态 props。把 class 改成 class="hot" 再编译,CLASS 消失;改成 v-bind="attrs" 后字段名也未知,编译器需要更宽的 FULL_PROPS 比较,不能继续保证只改一个字段。
Block Tree 解决另一层成本。若 section 下有静态 header、静态 article 容器,article 深处才有一个动态 p,普通递归需要经过静态祖先才能找到 p;block 在创建时收集这个动态后代,后续更新可以直接 patch 对应的动态节点。观察输出里的 openBlock/createElementBlock 与动态节点标记,而不只是数 _hoisted_。
给 article 增加 v-if="open" 后,内部结构可能整体出现/消失,会形成新的 block 边界。运行时先判断选中的结构,再处理该结构内的动态后代;不能把跨条件分支的 p 当作永远存在的同一槽位。编译器提供的工作清单依赖模板结构成立,不是任意 h() 树都能自动得到。
静态、运行时常量与错误边界
“对用户不常变”不等于编译期静态。{{ buildVersion }} 即使一生只赋值一次,编译器也只能把它当动态表达式;若确实只需首次值,可以在确认语义后用 v-once。相反,纯字面量结构可以自动缓存,不需要手写 memo。
直接写 render function 会保留动态表达能力,却通常丢失模板可提供的部分静态分析。JSX 插件可能做自己的优化,但不能假设它与模板输出完全相同;应检查当前工具链产物。
Patch Flag、dynamicChildren 等是理解机制的窗口,不是稳定业务 API。不要在应用逻辑中读取 VNode 内部字段作条件判断。
练习:做一次受控编译对照
编译两个模板:A 为 <div><span>fixed</span><b>{{ count }}</b></div>;B 把 fixed 改为 {{ label }}。输出 { cachedStatics, dynamicMarkers },前者统计 CACHED 标记或 _hoisted_ 声明,后者收集生成代码注释里的 TEXT|CLASS|PROPS。断言 A 的静态缓存多于 B,且 B 不会比 A 拥有更少的动态文本标记;不要断言完整生成代码字符串。
参考答案:第 7 章第 03 题。