FumadocsZDecode
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

On this page