Skip to content

第2.2节:完整的 DOM Diff 流程是怎样的?(上)

在上一节中,我们了解了 VNode 到 DOM 的转换过程。本节将深入探讨 Vue 3 中最核心的机制之一:DOM Diff 算法。当响应式数据发生变化时,Vue 如何高效地更新 DOM?这个过程涉及响应式触发、调度系统、组件更新和渲染优化等多个环节。

1. 更新机制概览

1.1 从数据变化到 DOM 更新的完整链路

数据变化 → 响应式触发 → 调度器入队 → 异步更新 → 组件重渲染 → DOM Diff → DOM 更新

让我们通过一个简单的例子来理解这个过程:

javascript
// 当我们修改响应式数据时
const state = reactive({ count: 0 })
state.count++ // 触发更新链路

1.2 核心源码文件结构

  • - 渲染器核心,包含 patch、diff 算法
  • - 调度系统,管理更新队列
  • - 组件渲染工具函数

2. 响应式触发:数据变化到更新队列

2.1 响应式系统的触发机制

当响应式数据发生变化时,会触发依赖收集的副作用函数。在组件中,这个副作用函数就是组件的更新函数:

typescript
// 在 setupRenderEffect 中创建的 ReactiveEffect
const effect = new ReactiveEffect(
  componentUpdateFn,
  () => queueJob(update), // scheduler
  instance.scope
)

2.2 组件更新函数的调度

函数中定义了组件的更新逻辑:
typescript
const componentUpdateFn = () => {
  if (!instance.isMounted) {
    // 初始挂载逻辑
    // ...
  } else {
    // 更新逻辑
    let { next, bu, u, parent, vnode } = instance
    
    if (next) {
      next.el = vnode.el
      updateComponentPreRender(instance, next, optimized)
    } else {
      next = vnode
    }
    
    // beforeUpdate 钩子
    if (bu) {
      invokeArrayFns(bu)
    }
    
    // 重新渲染组件
    const nextTree = renderComponentRoot(instance)
    const prevTree = instance.subTree
    instance.subTree = nextTree
    
    // 执行 patch 进行 diff
    patch(
      prevTree,
      nextTree,
      hostParentNode(prevTree.el!)!,
      getNextHostNode(prevTree),
      instance,
      parentSuspense,
      namespace
    )
  }
}

3. 调度系统:nextTick 和异步更新

3.1 更新队列管理

Vue 3 的调度系统通过 函数管理更新队列:

typescript
export function queueJob(job: SchedulerJob) {
  // 避免重复入队
  if (
    !queue.includes(
      job,
      isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
    )
  ) {
    if (job.id == null) {
      queue.push(job)
    } else {
      // 根据 id 插入到正确位置,保证父组件先于子组件更新
      queue.splice(findInsertionIndex(job.id), 0, job)
    }
    queueFlush()
  }
}

3.2 异步刷新机制

确保更新在下一个微任务中执行:
typescript
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

3.3 任务执行:flushJobs

负责执行队列中的所有更新任务:
typescript
function flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true
  
  // 排序确保父组件先于子组件更新
  queue.sort(comparator)
  
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job && job.active !== false) {
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0
    
    // 执行后置回调
    flushPostFlushCbs(seen)
    
    isFlushing = false
    currentFlushPromise = null
    
    // 递归处理新产生的任务
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}

4. 组件更新:updateComponent 流程

4.1 组件更新的触发条件

函数决定组件是否需要更新:
typescript
export function shouldUpdateComponent(
  prevVNode: VNode,
  nextVNode: VNode,
  optimized?: boolean,
): boolean {
  const { props: prevProps, children: prevChildren, component } = prevVNode
  const { props: nextProps, children: nextChildren, patchFlag } = nextVNode
  
  // 强制更新
  if (patchFlag & PatchFlags.FORCE_UPDATE) {
    return true
  }
  
  // 优化模式下的快速路径
  if (patchFlag & PatchFlags.DYNAMIC_SLOTS) {
    return hasPropsChanged(prevProps, nextProps, component!.emitsOptions)
  }
  
  // 检查 props 变化
  if (prevProps === nextProps) {
    return !!(prevChildren || nextChildren)
  }
  
  return hasPropsChanged(prevProps, nextProps, component!.emitsOptions)
}

4.2 组件实例的更新生命周期

函数处理组件更新:
typescript
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
  const instance = (n2.component = n1.component)!
  
  if (shouldUpdateComponent(n1, n2, optimized)) {
    instance.next = n2
    // 移除已排队的更新任务,避免重复更新
    invalidateJob(instance.update)
    // 触发组件更新
    instance.update()
  } else {
    // 无需更新,直接复用
    n2.el = n1.el
    instance.vnode = n2
  }
}

5. 子节点的 Diff 算法

5.1 patchChildren:子节点更新的入口

函数根据子节点类型选择不同的更新策略:
typescript
const patchChildren: PatchChildrenFn = (
  n1,
  n2,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  namespace,
  slotScopeIds,
  optimized = false,
) => {
  const c1 = n1 && n1.children
  const prevShapeFlag = n1 ? n1.shapeFlag : 0
  const c2 = n2.children
  const { patchFlag, shapeFlag } = n2
  
  // 快速路径:处理 patchFlag
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      // 有 key 的片段,使用完整 diff
      patchKeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      // 无 key 的片段,简单 diff
      patchUnkeyedChildren(
        c1 as VNode[],
        c2 as VNodeArrayChildren,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
      return
    }
  }
  
  // 根据新子节点类型处理
  if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
    // 新子节点是文本
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      // 卸载旧的数组子节点
      unmountChildren(c1 as VNode[], parentComponent, parentSuspense)
    }
    if (c2 !== c1) {
      // 设置文本内容
      hostSetElementText(container, c2 as string)
    }
  } else {
    if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 新旧都是数组,执行完整 diff
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      } else {
        // 新子节点为空,卸载旧子节点
        unmountChildren(c1 as VNode[], parentComponent, parentSuspense, true)
      }
    } else {
      // 旧子节点是文本或空
      if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
        hostSetElementText(container, '')
      }
      if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
        // 挂载新的数组子节点
        mountChildren(
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
      }
    }
  }
}

5.2 patchKeyedChildren:核心 Diff 算法

是 Vue 3 最核心的 diff 算法,采用了双端比较 + 最长递增子序列的优化策略:
typescript
const patchKeyedChildren = (
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
) => {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // 旧子节点的结束索引
  let e2 = l2 - 1 // 新子节点的结束索引
  
  // 1. 从开始位置同步
  // (a b) c
  // (a b) d e
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = (c2[i] = optimized
      ? cloneIfMounted(c2[i] as VNode)
      : normalizeVNode(c2[i]))
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      break
    }
    i++
  }
  
  // 2. 从结束位置同步
  // a (b c)
  // d e (b c)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = (c2[e2] = optimized
      ? cloneIfMounted(c2[e2] as VNode)
      : normalizeVNode(c2[e2]))
    if (isSameVNodeType(n1, n2)) {
      patch(
        n1,
        n2,
        container,
        null,
        parentComponent,
        parentSuspense,
        namespace,
        slotScopeIds,
        optimized,
      )
    } else {
      break
    }
    e1--
    e2--
  }
  
  // 3. 公共序列 + 挂载
  // (a b)
  // (a b) c
  // i = 2, e1 = 1, e2 = 2
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
      while (i <= e2) {
        patch(
          null,
          (c2[i] = optimized
            ? cloneIfMounted(c2[i] as VNode)
            : normalizeVNode(c2[i])),
          container,
          anchor,
          parentComponent,
          parentSuspense,
          namespace,
          slotScopeIds,
          optimized,
        )
        i++
      }
    }
  }
  
  // 4. 公共序列 + 卸载
  // (a b) c
  // (a b)
  // i = 2, e1 = 2, e2 = 1
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }
  
  // 5. 未知序列
  // [i ... e1 + 1]: a b [c d e] f g
  // [i ... e2 + 1]: a b [e d c h] f g
  // i = 2, e1 = 4, e2 = 5
  else {
    // 这里是最复杂的情况,需要使用最长递增子序列算法
    // 详细实现见下一节
  }
}

6. 渲染优化:静态提升和 Patch Flags

6.1 Patch Flags 优化

Vue 3 编译器会为 VNode 添加 patchFlag,标记节点的动态部分:

typescript
export const enum PatchFlags {
  TEXT = 1,                    // 动态文本内容
  CLASS = 1 << 1,             // 动态 class
  STYLE = 1 << 2,             // 动态 style
  PROPS = 1 << 3,             // 动态属性
  FULL_PROPS = 1 << 4,        // 具有动态 key 的属性
  HYDRATE_EVENTS = 1 << 5,    // 事件监听器
  STABLE_FRAGMENT = 1 << 6,   // 稳定的片段
  KEYED_FRAGMENT = 1 << 7,    // 有 key 的片段
  UNKEYED_FRAGMENT = 1 << 8,  // 无 key 的片段
  NEED_PATCH = 1 << 9,        // 需要 patch
  DYNAMIC_SLOTS = 1 << 10,    // 动态插槽
  DEV_ROOT_FRAGMENT = 1 << 11, // 开发模式根片段
  HOISTED = -1,               // 静态提升
  BAIL = -2,                  // diff 算法回退
}

6.2 静态提升优化

编译器会将静态节点提升到渲染函数外部,避免重复创建:

javascript
// 编译前
<template>
  <div>
    <h1>静态标题</h1>
    <p>{{ message }}</p>
  </div>
</template>

// 编译后(简化)
const _hoisted_1 = createElementVNode("h1", null, "静态标题")

function render() {
  return createElementVNode("div", null, [
    _hoisted_1, // 静态节点被提升
    createElementVNode("p", null, toDisplayString(message), 1 /* TEXT */)
  ])
}

7. 父子组件更新顺序

7.1 更新队列的排序机制

Vue 3 通过组件的 id 确保父组件先于子组件更新:

typescript
const getId = (job: SchedulerJob): number =>
  job.id == null ? Infinity : job.id

const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
  const diff = getId(a) - getId(b)
  if (diff === 0) {
    if (a.pre && !b.pre) return -1
    if (b.pre && !a.pre) return 1
  }
  return diff
}

7.2 组件 ID 的分配

组件实例创建时会分配递增的 ID:

typescript
let uid = 0

export function createComponentInstance(
  vnode: VNode,
  parent: ComponentInternalInstance | null,
  suspense: SuspenseBoundary | null,
) {
  const instance: ComponentInternalInstance = {
    uid: uid++, // 递增的唯一 ID
    vnode,
    type: vnode.type,
    parent,
    // ...
  }
  
  return instance
}

小结

本节我们深入分析了 Vue 3 DOM Diff 流程的前半部分,包括:

  1. 响应式触发机制:从数据变化到更新队列的完整链路
  2. 调度系统:异步更新和任务队列管理
  3. 组件更新流程:shouldUpdateComponent 和 updateComponent 的工作原理
  4. 子节点 Diff:patchChildren 和 patchKeyedChildren 的基本策略
  5. 渲染优化:Patch Flags 和静态提升的优化机制
  6. 更新顺序:父子组件的更新顺序保证

在下一节中,我们将继续深入探讨 patchKeyedChildren 中最复杂的"未知序列"处理逻辑,以及最长递增子序列算法的具体实现和优化原理。


微信公众号二维码