Vue
PatchFlag 与 diff 优化
背景
Vue 2 的 diff 是全量对比:每次更新都要递归遍历整棵 VNode 树,逐个比较属性、子节点,即使大部分节点是静态的也无法跳过。
Vue 3 在编译阶段分析模板,给动态节点打上 PatchFlag,运行时 diff 只处理有标记的节点,静态内容完全跳过。
一、PatchFlag 是什么
PatchFlag 是一个整数位掩码,编译时写入 VNode,标记该节点哪些部分是动态的。
// packages/shared/src/patchFlags.ts(简化)
export const enum PatchFlags {
TEXT = 1, // 动态文本
CLASS = 1 << 1, // 动态 class
STYLE = 1 << 2, // 动态 style
PROPS = 1 << 3, // 动态 props(非 class/style)
FULL_PROPS = 1 << 4, // 有动态 key,需全量对比 props
NEED_HYDRATION = 1 << 5, // 需要 hydration
STABLE_FRAGMENT = 1 << 6, // 子节点顺序不变的 Fragment
KEYED_FRAGMENT = 1 << 7, // 带 key 的列表(v-for + key)
UNKEYED_FRAGMENT = 1 << 8, // 无 key 的列表
NEED_PATCH = 1 << 9, // 需要 patch(ref / 指令等)
DYNAMIC_SLOTS = 1 << 10, // 动态插槽
HOISTED = -1, // 静态提升节点,跳过 diff
BAIL = -2, // 退出优化,走全量 diff
}多个标记可以用位或合并:TEXT | CLASS = 3,diff 时用位与判断:
if (patchFlag & PatchFlags.TEXT) {
/* 只更新文本 */
}
if (patchFlag & PatchFlags.CLASS) {
/* 只更新 class */
}二、编译阶段如何打标记
模板:
<div>
<span class="static">静态文本</span>
<span :class="cls" :id="id">{{ msg }}</span>
</div>编译产物(简化):
import {
createElementVNode as _c,
toDisplayString as _s,
normalizeClass as _nc,
openBlock as _ob,
createElementBlock as _ceb,
} from 'vue'
// 静态节点提升到渲染函数外部,只创建一次
const _hoisted_1 = _c('span', { class: 'static' }, '静态文本')
function render(_ctx) {
return (
_ob(),
_ceb('div', null, [
_hoisted_1, // 直接复用,不参与 diff
_c(
'span',
{
class: _nc(_ctx.cls),
id: _ctx.id,
},
_s(_ctx.msg),
3 /* TEXT | CLASS */, // ← PatchFlag = 1 | 2 = 3
// 注意:id 是动态 prop 但不是 class/style,
// 实际上编译器会用 PROPS 并列出 dynamicProps: ['id']
),
])
)
}编译器在 AST 分析阶段就确定了每个节点的动态性,generate 时直接把 PatchFlag 写入 createElementVNode 的第四个参数。
三、运行时如何利用 PatchFlag
patchElement 函数根据 PatchFlag 决定做什么:
function patchElement(n1, n2) {
const { patchFlag, dynamicProps } = n2
if (patchFlag > 0) {
// 有 PatchFlag,走优化路径
if (patchFlag & PatchFlags.FULL_PROPS) {
patchProps(el, n2, n1.props, n2.props) // 全量对比 props
} else {
if (patchFlag & PatchFlags.CLASS) {
if (n1.props.class !== n2.props.class) {
hostPatchProp(el, 'class', null, n2.props.class)
}
}
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(el, 'style', n1.props.style, n2.props.style)
}
if (patchFlag & PatchFlags.PROPS) {
// 只遍历 dynamicProps,不是全部 props
for (let i = 0; i < dynamicProps.length; i++) {
const key = dynamicProps[i]
hostPatchProp(el, key, n1.props[key], n2.props[key])
}
}
}
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(el, n2.children)
}
}
} else {
// 无 PatchFlag(patchFlag = 0 或负数),全量对比
patchProps(el, n2, n1.props, n2.props)
}
if (patchFlag & PatchFlags.NEED_PATCH) {
patchAttrs(n1, n2) // 处理 ref、指令等
}
}四、Block Tree(块树)
仅靠 PatchFlag 还不够——diff 仍需遍历整棵树才能找到有标记的节点。Vue 3 引入了 Block Tree 来解决这个问题。
核心思想:每个 Block(组件根节点或 v-if / v-for 的容器)维护一个 dynamicChildren 数组,收集其子树中所有动态节点的扁平列表。
// openBlock 创建当前 block 的动态节点收集栈
let currentBlock = []
function openBlock() {
blockStack.push((currentBlock = []))
}
// createElementBlock 在关闭 block 时,把收集到的动态子节点存入 VNode
function createElementBlock(type, props, children, patchFlag) {
const vnode = createElementVNode(type, props, children, patchFlag)
vnode.dynamicChildren = currentBlock // 扁平化的动态节点列表
blockStack.pop()
currentBlock = blockStack[blockStack.length - 1]
if (currentBlock) currentBlock.push(vnode)
return vnode
}diff 时直接遍历 dynamicChildren:
function patchBlockChildren(oldChildren, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
patchElement(oldChildren[i], newChildren[i])
// 每个节点都有 PatchFlag,精确更新
}
}无论模板多深,diff 只需一次扁平遍历,时间复杂度从 O(树节点数) 降到 O(动态节点数)。
五、静态提升(Static Hoisting)
没有任何动态绑定的节点,编译器将其提升到渲染函数外部,组件重新渲染时直接复用同一个 VNode 对象,既不重新创建也不参与 diff。
// 渲染函数外部,模块初始化时只执行一次
const _hoisted_1 = createElementVNode('p', null, '我永远不变')
const _hoisted_2 = createElementVNode('span', { class: 'icon' }, '★')
function render() {
return createElementBlock('div', null, [
_hoisted_1, // 直接引用,跳过创建和 diff
_hoisted_2,
createElementVNode('p', null, _ctx.dynamic, 1 /* TEXT */),
])
}六、完整优化链路总结
编译阶段
↓ parse:template → AST
↓ transform:分析动态性,标记 PatchFlag,收集 dynamicProps
↓ generate:静态节点提升到函数外,动态节点写入 PatchFlag
运行时
↓ render:执行渲染函数,openBlock 收集动态子节点 → dynamicChildren
↓ diff:patchBlockChildren 只遍历 dynamicChildren(扁平列表)
↓ patchElement:根据 PatchFlag 位掩码,只更新变化的那一项| 优化手段 | 作用 |
|---|---|
| PatchFlag | 精确知道节点哪个部分变了,避免全量 props 对比 |
| dynamicProps | 只对比声明为动态的 prop,不遍历全部 |
| Block Tree | 扁平化动态节点,diff 不需要遍历整棵树 |
| 静态提升 | 静态节点只创建一次,完全跳过 diff |