This site is no longer maintained. Please visit zdecode.vercel.app
ZDecode
Vue

Vue 3 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