Skip to content

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 模板编译器包:

js
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 题。

用心记录每一步成长