Patch Flags(补丁标记)

Vue 3 模板编译器为动态节点生成的位标记常量(如 TEXT=1, CLASS=2),运行时 diff 通过位运算跳过不可能变化的属性,将 diff 复杂度从 O(template) 压缩到 O(dynamic bindings)。本质上是编译器给渲染管线留的"快捷通道"。

核心概念

Patch Flags 是 Vue 3 模板编译器在编译阶段为每个动态 VNode 生成的位标记(bitmask)。它告诉运行时 diff 算法"这个节点的哪些部分是动态的",使 patchElement 函数能通过位运算直接跳过静态属性的比对。

这是典型的编译器-运行时协作:编译器在 createElementVNode 的第四个参数位置注入 Patch Flag,运行时读取并据此选择最短的 diff 路径。

常量定义

源码位置:packages/shared/src/patchFlags.ts

ts
export const enum PatchFlags {
  TEXT = 1,             // 动态文本内容
  CLASS = 1 << 1,       // 2 — 动态 class
  STYLE = 1 << 2,       // 4 — 动态 style
  PROPS = 1 << 3,       // 8 — 动态非 class/style 的 props
  FULL_PROPS = 1 << 4,  // 16 — 有动态 key,需全量 diff
  NEED_HYDRATION = 1 << 5, // 32 — 需要水合的事件监听
  STABLE_FRAGMENT = 1 << 6, // 64 — 子节点顺序不变
  KEYED_FRAGMENT = 1 << 7,  // 128 — 有 key 的 fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 — 无 key 的 fragment
  NEED_PATCH = 1 << 9,  // 512 — 需非 props patch(ref/指令)
  DYNAMIC_SLOTS = 1 << 10, // 1024 — 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, // 2048 — 仅开发环境
  HOISTED = -1,         // 静态提升节点,diff 直接跳过
  BAIL = -2             // 优化退出,走全量比较
}

编译示例

模板:

html
<div :class="cls">static text</div>

编译产物:

js
createElementVNode("div", { class: _ctx.cls }, "static text", 2 /* CLASS */)

末尾的 2PatchFlags.CLASS。运行时 patchElement 检测到此标记后,只比对 class 属性,textContent 已知是静态的直接跳过。

运行时使用方式

renderer.tspatchElement 内部通过位运算判断:

js
if (patchFlag & PatchFlags.CLASS) {
  // 只 diff class
}
if (patchFlag & PatchFlags.STYLE) {
  // 只 diff style
}

多个动态属性通过位或组合:一个同时有动态 class 和 style 的节点,patchFlag = CLASS | STYLE = 6

配合机制

  • Static Hoisting(静态提升)HOISTED = -1 标记的 VNode 被提升到渲染函数外部,每次渲染复用同一个引用,diff 时直接跳过。
  • Block Tree:编译器将模板按 v-if / v-for 切分为 Block,每个 Block 只追踪内部的动态节点(带 Patch Flag 的),静态节点不进入 diff 流程。
  • Hydration 加速:Patch Flags 在水合路径上价值尤其大——hydrateElement 根据标记只检查动态属性是否与服务端一致,静态部分直接信任,大幅减少水合比对工作量。

局限性

Patch Flags 仅对模板语法生效。Vue 中使用 JSX 编写的渲染函数不会获得 Patch Flags 优化,因为 JSX 的动态性无法在编译期静态分析——这是 Vue 模板和 JSX 两种渲染模式优化程度不一致的根本原因。

引用本术语的文章