Appearance
编译优化:Block、PatchFlags 与静态提升
在前面几节中,我们完成了 parse(解析)和 transform(转换)。transform 阶段的主要目的,是在 AST 中嵌入“优化信息”。
Vue 2 的 diff 算法需要遍历整棵 VNode 树来找出差异。Vue 3 的编译器则会在编译时(transform 阶段)预先分析出模板中所有“动态”和“静态”的部分,并为运行时 diff 算法提供两种核心的优化信息:
- 静态提升 (Static Hoisting)
- 补丁标记 (Patch Flags)
而 Block(块) 则是应用这些优化信息的基础容器。本节将说明这三者是如何协同工作的。
优化一:静态提升 (Static Hoisting)
静态提升是 Vue 3 一项重要的优化策略。
目标:找出模板中完全不会改变的节点。
策略: transform 阶段会调用 getConstantType 函数,分析 AST 节点。如果一个节点(包括它的所有 props 和 children)都是静态的,它就会被标记为 ConstantTypes.CAN_CACHE。
codegen(代码生成)阶段: 当 codegen 遇到一个被标记为 CAN_CACHE 的节点时,会执行以下操作:
- 将这个节点的 VNode 创建代码从
render函数中“提升”出去,作为const常量在模块顶层只创建一次。 - 在
render函数中,直接复用这个常量。
示例:
模板:
html<div> <p>我是静态的</p> <span>{{ msg }}</span> </div>codegen生成的 JS 代码 (简化后):javascript// 1. "p" 节点被分析为静态,被“提升” const _hoisted_1 = /*#__PURE__*/ createStaticVNode("<p>我是静态的</p>", 1) export function render(_ctx, _cache) { return (openBlock(), createElementBlock("div", null, [ // 2. render 函数中,直接“复用” _hoisted_1 _hoisted_1, // 3. "span" 节点是动态的,在 render 内部创建 createElementVNode("span", null, toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }
效果: 当组件重新渲染时,render 函数被再次调用。对于 <span>,它需要重新创建 VNode。但对于 <p>,它无需任何操作,只是复用了 _hoisted_1 常量。VNode 的创建开销被免除。
优化二:Patch Flags (靶向更新)
静态提升只能处理 100% 静态的节点。但大部分节点是“部分动态”的。
目标:为“部分动态”的节点,提供一个“靶向更新”的提示。
策略: transform 阶段(主要是 transformElement 和 buildProps)会分析一个元素“哪里可能会变”。然后,它会通过位运算,将这些“变化点”组合成一个数字,这就是 Patch Flag。
示例:
<div :class="cls">- 分析:只有
class是动态的。 patchFlag:2(即PatchFlags.CLASS)- 运行时:
diff算法看到2,会跳过所有其他props和children的对比,只检查class属性。
- 分析:只有
<div></div>- 分析:只有
children中的文本是动态的。 patchFlag:1(即PatchFlags.TEXT)- 运行时:
diff算法看到1,会跳过所有props对比,只检查textContent。
- 分析:只有
<div :id="id" :class="cls">- 分析:
id和class都是动态的。 patchFlag:10(即PatchFlags.CLASS (2) | PatchFlags.PROPS (8))- 运行时:
diff算法看到10,知道要检查class和PROPS。
- 分析:
<div :[dynamicKey]="val">- 分析:
props的键名是动态的。 patchFlag:16(即PatchFlags.FULL_PROPS)- 运行时:
diff算法看到16,知道必须进行全量的props对比,优化失效。
- 分析:
Patch Flags 是编译器给运行时的“精确提示”,它将 diff 的开销从“遍历所有”降低为“只检查标记的地方”。
核心概念:Block (动态节点容器)
有了“静态提升”和“Patch Flags”,它们如何在一个复杂的树结构中协同工作?
场景:
html
<div :id="id"> <p>static 1</p> <p>static 2</p> <span>{{ msg }}</span> <p>static 3</p> </div>static 1, 2, 3会被静态提升(Hoisted)。<span>会被打上PatchFlags.TEXT。<div>会被打上PatchFlags.PROPS(因为它有动态id)。
问题:当重新渲染时,diff 算法如何跳过那 3 个静态的 <p> 节点,只 diff 那个 <span> 呢?
答案:这就是 Block(块) 的作用。
transform 阶段会识别出,<div> 是一个“Block 节点”(因为它有动态的子孙节点)。在 codegen 时,它会用 createElementBlock (而不是 createElementVNode) 来创建它。
一个 Block VNode 是一个特殊的 VNode,它有两个特征:
- 它本身会携带
patchFlag(例如<div>上的PatchFlags.PROPS)。 - 它包含一个额外的数组:
dynamicChildren。
transform 在分析 <div> 时,会跳过所有静态子节点(static 1, 2, 3),只把动态的子孙节点(<span>)收集到 dynamicChildren 数组中。
codegen 生成的代码 (概念):
javascript
const _hoisted_1 = createStaticVNode("<p>static 1</p>", 1)
const _hoisted_2 = createStaticVNode("<p>static 2</p>", 1)
const _hoisted_3 = createStaticVNode("<p>static 3</p>", 1)
function render(_ctx, _cache) {
return (
// 1. 创建一个 "Block"
_openBlock(),
_createElementBlock("div", { id: _ctx.id }, [
// 2. Block 的“普通 children”包含所有节点
_hoisted_1,
_hoisted_2,
// 3. "span" 是动态的,在内部创建
_createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
_hoisted_3
],
// 4. Block 携带“自己的” PatchFlag (只 diff props)
8 /* PROPS */,
// 5. 【关键】Block 携带一个“动态子节点”列表!
// (这个列表在编译时生成,内容指向 <span>)
["id"] // 这里的 dynamicChildren 仅为示意
))
}运行时的 diff 过程: 当 diff 算法遇到这个 div “Block”时:
- 看到
patchFlag: 8 /* PROPS */,于是它只diff了id属性。 - 然后,它完全跳过了对
div的children数组(包含4个元素)的遍历。 - 它转而只遍历
dynamicChildren数组(只包含1个元素<span>),并对其进行diff。
Block + Patch Flags 将 diff 算法的复杂度从 O(n) (遍历所有节点) 降低到了 O(d) (只遍历动态节点)。
总结
Vue 3 的编译优化是一个协同工作的系统,它将“编译时分析”与“运行时执行”相结合:
静态提升 (Static Hoisting):
- 工作:找出 100% 静态的 VNode,将其创建提升到
render函数之外。 - 目标:跳过 VNode 创建,实现复用。
- 工作:找出 100% 静态的 VNode,将其创建提升到
Patch Flags (补丁标记):
- 工作:为“部分动态”的 VNode 打上“提示”标记(如
CLASS,TEXT,PROPS)。 - 目标:跳过属性对比,实现“靶向更新”。
- 工作:为“部分动态”的 VNode 打上“提示”标记(如
Block (动态节点容器):
- 工作:创建一个特殊的 VNode,它“收集”所有动态的子孙节点。
- 目标:跳过子节点遍历,让
diff只在动态节点之间进行。
这三大技术协同工作,使得 Vue 3 的 diff 算法能够跳过大量静态内容,只对标记的动态部分执行更新,从而实现了显著的性能提升。
