Skip to content

编译优化:Block、PatchFlags 与静态提升

在前面几节中,我们完成了 parse(解析)和 transform(转换)。transform 阶段的主要目的,是在 AST 中嵌入“优化信息”

Vue 2 的 diff 算法需要遍历整棵 VNode 树来找出差异。Vue 3 的编译器则会在编译时(transform 阶段)预先分析出模板中所有“动态”和“静态”的部分,并为运行时 diff 算法提供两种核心的优化信息:

  1. 静态提升 (Static Hoisting)
  2. 补丁标记 (Patch Flags)

Block(块) 则是应用这些优化信息的基础容器。本节将说明这三者是如何协同工作的。

优化一:静态提升 (Static Hoisting)

静态提升是 Vue 3 一项重要的优化策略。

目标:找出模板中完全不会改变的节点。

策略transform 阶段会调用 getConstantType 函数,分析 AST 节点。如果一个节点(包括它的所有 propschildren)都是静态的,它就会被标记为 ConstantTypes.CAN_CACHE

codegen(代码生成)阶段: 当 codegen 遇到一个被标记为 CAN_CACHE 的节点时,会执行以下操作:

  1. 将这个节点的 VNode 创建代码render 函数中“提升”出去,作为 const 常量在模块顶层只创建一次
  2. 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 阶段(主要是 transformElementbuildProps)会分析一个元素“哪里可能会变”。然后,它会通过位运算,将这些“变化点”组合成一个数字,这就是 Patch Flag

示例:

  • <div :class="cls">

    • 分析:只有 class 是动态的。
    • patchFlag2 (即 PatchFlags.CLASS)
    • 运行时diff 算法看到 2,会跳过所有其他 propschildren 的对比,检查 class 属性。
  • <div></div>

    • 分析:只有 children 中的文本是动态的。
    • patchFlag1 (即 PatchFlags.TEXT)
    • 运行时diff 算法看到 1,会跳过所有 props 对比,检查 textContent
  • <div :id="id" :class="cls">

    • 分析idclass 都是动态的。
    • patchFlag10 (即 PatchFlags.CLASS (2) | PatchFlags.PROPS (8))
    • 运行时diff 算法看到 10,知道要检查 classPROPS
  • <div :[dynamicKey]="val">

    • 分析props键名是动态的。
    • patchFlag16 (即 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>
  1. static 1, 2, 3 会被静态提升(Hoisted)。
  2. <span> 会被打上 PatchFlags.TEXT
  3. <div> 会被打上 PatchFlags.PROPS (因为它有动态 id)。

问题:当重新渲染时,diff 算法如何跳过那 3 个静态的 <p> 节点, diff 那个 <span> 呢?

答案:这就是 Block(块) 的作用。

transform 阶段会识别出,<div> 是一个“Block 节点”(因为它有动态的子孙节点)。在 codegen 时,它会用 createElementBlock (而不是 createElementVNode) 来创建它。

一个 Block VNode 是一个特殊的 VNode,它有两个特征:

  1. 它本身会携带 patchFlag(例如 <div> 上的 PatchFlags.PROPS)。
  2. 它包含一个额外的数组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”时:

  1. 看到 patchFlag: 8 /* PROPS */,于是它只 diffid 属性。
  2. 然后,它完全跳过了对 divchildren 数组(包含4个元素)的遍历。
  3. 它转而只遍历 dynamicChildren 数组(只包含1个元素 <span>),并对其进行 diff

Block + Patch Flagsdiff 算法的复杂度从 O(n) (遍历所有节点) 降低到了 O(d) (只遍历动态节点)


总结

Vue 3 的编译优化是一个协同工作的系统,它将“编译时分析”与“运行时执行”相结合:

  1. 静态提升 (Static Hoisting)

    • 工作:找出 100% 静态的 VNode,将其创建提升到 render 函数之外。
    • 目标跳过 VNode 创建,实现复用。
  2. Patch Flags (补丁标记)

    • 工作:为“部分动态”的 VNode 打上“提示”标记(如 CLASS, TEXT, PROPS)。
    • 目标跳过属性对比,实现“靶向更新”。
  3. Block (动态节点容器)

    • 工作:创建一个特殊的 VNode,它“收集”所有动态的子孙节点。
    • 目标跳过子节点遍历,让 diff 只在动态节点之间进行。

这三大技术协同工作,使得 Vue 3 的 diff 算法能够跳过大量静态内容,只对标记的动态部分执行更新,从而实现了显著的性能提升。


微信公众号二维码

Last updated: