Skip to content

D04 · 编译优化:PatchFlag 与 Block Tree ​

对应主课: L34 编译器优化 最后核对: 2026-09-23 与 L34 的关系: L34 侧重"看懂编译输出",本篇侧重"为什么这样编译"和"动手验证"


1. Vue 3 编译器三阶段 ​

本篇固定 @vue/compiler-dom 3.5.43。模板编译可分为 parse、transform、generate;SFC 还包含脚本、样式与绑定元数据的处理,不能把模板编译等同于整个 .vue 文件的构建:

vue
<template>
  <div class="card">
    <h3>标题</h3>                    <!-- 静态 -->
    <p>{{ description }}</p>         <!-- 动态文本 -->
    <span :class="statusClass">ok</span>  <!-- 动态 class -->
  </div>
</template>

对去掉空白和注释后的同一模板,使用 { mode: 'module', hoistStatic: true, cacheHandlers: true } 得到:

javascript
import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

const _hoisted_1 = { class: "card" }

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("div", _hoisted_1, [
    _cache[0] || (_cache[0] = _createElementVNode("h3", null, "标题", -1 /* CACHED */)),
    _createElementVNode("p", null, _toDisplayString(_ctx.description), 1 /* TEXT */),
    _createElementVNode("span", {
      class: _normalizeClass(_ctx.statusClass)
    }, "ok", 2 /* CLASS */)
  ]))
}

静态 h3 位于当前实例的渲染缓存,静态 props 对象位于 render 外;这两者都减少重复分配,但生命周期与共享范围不同。§5 的本地脚本可重现输出。


2. PatchFlag 全量表 ​

以下是 Vue 3.5.43 的内部标记,用于选择相应优化路径,不是独立决定所有渲染行为的开关:

Flag值含义
TEXT1动态文本
CLASS2元素的动态 class
STYLE4动态 style
PROPS8检查 dynamicProps 列出的绑定属性
FULL_PROPS16属性名集合可能变化,需要完整比较 props;不是 VNode 的 key
NEED_HYDRATION32需要属性水合,如部分事件/prop 绑定
STABLE_FRAGMENT64子节点顺序稳定的 Fragment
KEYED_FRAGMENT128有 key 或部分有 key 的子节点
UNKEYED_FRAGMENT256无 key 子节点
NEED_PATCH512ref、指令等非普通 props 更新需要纳入 patch
DYNAMIC_SLOTS1024带动态插槽的组件需要更新
DEV_ROOT_FRAGMENT2048开发模式根注释产生的 Fragment
CACHED-1缓存静态 VNode 的特殊标记
BAIL-2退出当前优化模式,采用更完整的比较

正数标记可按位组合,例如 TEXT | CLASS 为 3;负数特殊标记用相等判断,不能把 -1 当作“所有正数位都启用”。3.4 的 HOISTED 名称不要直接套进这份 3.5 枚举。3.5.43 patchFlags.ts

位运算组合 ​

PatchFlag 使用位运算组合多种动态性:

javascript
// 同时有动态文本 + 动态 class
// TEXT(1) | CLASS(2) = 3
createVNode("p", { class: cls }, text, 3 /* TEXT | CLASS */)

// 正数动态标记才按位判断;CACHED/BAIL 等特殊值另作相等判断。
if (patchFlag > 0) {
  if (patchFlag & 1) { /* 检查文本 */ }
  if (patchFlag & 2) { /* 检查 class */ }
}
// 互不影响,一次位运算就能判断

3. 静态缓存、常量提升与事件缓存 ​

§1 的 h3 第一次 render 时创建,以后从实例的 _cache[0] 复用。两个组件实例各有自己的 renderCache,不能在模块顶层手写一个 VNode 后,假定它可零成本在所有实例和位置复用。Vue 3.4 的 HOISTED 示例属于另一版输出,3.5 的静态 props 等常量仍可提升。3.5 cacheStatic

事件处理器缓存又是另一种优化。对 <button @click="handleClick">Click</button> 启用 cacheHandlers 后,实际生成:

javascript
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createElementBlock("button", {
    onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.handleClick && _ctx.handleClick(...args)))
  }, "Click"))
}

缓存的是当前实例的包装函数;调用时仍会读取当时的 _ctx.handleClick。这不要求业务处理器“纯静态、不能读取动态状态”,也不是把某个实例的方法提升成模块共享函数。循环变量、绑定元数据与编译选项还会影响能否缓存。vOn 编译转换


4. Block Tree ​

编译器生成 openBlock/createElementBlock 等调用,运行时据此收集当前 block 的动态后代或子 block 到 dynamicChildren。结构对应稳定时,patch 可以直接处理这组节点,跳过静态子树:

页面有 100 个节点,3 个是动态的:

假设一个结构稳定 block 只有 3 个需检查的动态后代:
优化路径可处理这 3 个 dynamicChildren,跳过其间的静态层级。
这说明减少的节点检查,不是整页快了 100 / 3 倍。

Block 的结构不稳定问题 ​

条件与列表可能改变结构。编译器用 branch/Fragment 等 block 边界表达变化,列表仍需按有无 key 等情况协调;并不是给所有 v-for 情况统一套一个不变数组:


5. 动手实验:Vue Playground 验证 ​

实验 1:观察 PatchFlag ​

  1. 打开 Vue SFC Playground
  2. 把 Vue 版本切换到 3.5.43,再查看编译后的 JavaScript 面板;界面位置可能变化
  3. 输入以下模板,观察编译输出中的数字注释:
vue
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
const cls = ref('active')
</script>

<template>
  <div>
    <h1>静态标题</h1>
    <p>{{ msg }}</p>
    <span :class="cls">文字</span>
    <a :href="'/page/' + msg" :class="cls">链接</a>
  </div>
</template>

预期观察:

  • h1 → CACHED (-1),首次写入当前实例渲染缓存,以后复用
  • p → TEXT (1),只有文本是动态的
  • span → CLASS (2),只有 class 是动态的
  • a → PROPS|CLASS (10),即 8 + 2,href 和 class 都是动态的

实验 2:固定版本,检查生成结构 ​

继续使用 L31–L37 的 labs/mini-reactivity,Vue 与 compiler-dom 都固定 3.5.43(编译器已在 L34 安装)。保存 compile-check.mjs,运行 node compile-check.mjs。这段只在本地对已知模板执行生成函数,不把外部输入交给 new Function:

javascript
// labs/mini-reactivity/compile-check.mjs
import assert from 'node:assert/strict'
import * as Vue from 'vue'
import { compile } from '@vue/compiler-dom'
const card = '<div class="card"><h3>标题</h3><p>{{ description }}</p><span :class="statusClass">ok</span></div>'
const flags = '<div><h1>静态标题</h1><p>{{ msg }}</p><span :class="cls">文字</span><a :href="\'/page/\' + msg" :class="cls">链接</a></div>'
const event = '<button @click="handleClick">Click</button>'
for (const template of [card, flags, event]) {
  console.log(compile(template, { mode: 'module', hoistStatic: true, cacheHandlers: true }).code)
}
function makeRender(template) {
  const { code } = compile(template, {
    mode: 'function', prefixIdentifiers: true, hoistStatic: true, cacheHandlers: true,
  })
  return new Function('Vue', code)(Vue)
}
const render = makeRender(flags)
const cache = []
const first = render({ msg: 'hello', cls: 'active' }, cache)
const second = render({ msg: 'world', cls: 'active' }, cache)
assert.equal(first.children[0], second.children[0])
assert.equal(first.children[0].patchFlag, -1)
assert.deepEqual(first.dynamicChildren.map(node => node.patchFlag), [1, 2, 10])
assert.equal(second.children[1].children, 'world')
const anotherInstance = render({ msg: 'other', cls: 'active' }, [])
assert.notEqual(first.children[0], anotherInstance.children[0])

const renderEvent = makeRender(event)
const eventCache = []
let called = ''
const context = { handleClick: () => { called = 'first' } }
const button1 = renderEvent(context, eventCache)
context.handleClick = () => { called = 'second' }
const button2 = renderEvent(context, eventCache)
assert.equal(button1.props.onClick, button2.props.onClick)
button2.props.onClick()
assert.equal(called, 'second')
console.log('缓存、动态标记与事件包装函数检查通过')

这里检查的是生成的 VNode/缓存结构,没有实际挂载 DOM,也没有测量耗时;实际组件挂载验证 DOM 行为,L30 的测量流程则用于评估性能。


6. 性能实际影响 ​

优化减少的工作仍然存在的工作
动态标记避免逐个检查已知不会变化的属性动态表达式求值、相应 DOM 操作
静态缓存避免反复分配同一实例的静态 VNode首次创建、缓存保存、实际 DOM 生命周期
Block跳过稳定结构中的静态子树检查block 边界、列表协调、组件更新与回退路径

不能把比较次数直接换算成速度提升。原生 DOM、布局绘制、组件状态、数据获取与设备差异都会影响实测结果;空循环或一边创建对象一边只比较常量的计时,既不等价,也可能被引擎优化掉。按 L30 固定场景测量,记录优化前后条件与结果,才有依据报告倍数。


7. 总结 ​

  • PatchFlag 让 diff 精确到属性级别,用位运算组合多种动态性
  • 3.5 静态 VNode 缓存与常量提升分属不同机制,事件包装函数也可按实例缓存
  • 稳定 block 可只检查动态后代;这不是整个应用或 keyed diff 的统一复杂度结论
  • 条件与列表有相应结构边界,仍需协调增删与移动
  • 编译器提供提示,运行时利用提示执行优化;手写 h、动态结构等可能使用不同路径
  • 用固定版本本地脚本重现输出,再用浏览器与测量工具验证真实行为和性能