Vue3 虚拟 DOM diff 算法的源码解析

一、核心流程图

有key
无key
patchChildren
是否存在 key
patchKeyedChildren
patchUnkeyedChildren
预处理阶段
双端对比
处理新增节点
处理删除节点
建立索引映射
计算最长递增子序列
移动/新增节点

二、核心代码解析

1. 入口函数:patchChildren (runtime-core/src/renderer.ts)
const patchChildren: PatchChildrenFn = (/*...*/) => {
  if (patchFlag > 0) {
    if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
      patchKeyedChildren(/*...*/)
      return
    } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
      patchUnkeyedChildren(/*...*/)
      return
    }
  }
  // ...其他处理逻辑
}
2. 核心算法:patchKeyedChildren (runtime-core/src/renderer.ts)

实现步骤:

function patchKeyedChildren(
  c1: VNode[],
  c2: VNodeArrayChildren,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  isSVG: boolean,
  slotScopeIds: string[] | null,
  optimized: boolean
) {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1 // prev ending index
  let e2 = l2 - 1 // next ending index

  // 1. 双端预处理
  // (a) 从头部同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      patch(/*...*/)
    } else {
      break
    }
    i++
  }

  // (b) 从尾部同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(/*...*/)
    } else {
      break
    }
    e1--
    e2--
  }

  // 2. 处理新增节点
  if (i > e1) {
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor
      while (i <= e2) {
        patch(null, c2[i], container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized)
        i++
      }
    }
  }

  // 3. 处理删除节点
  else if (i > e2) {
    while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }

  // 4. 复杂序列处理
  else {
    const s1 = i // prev starting index
    const s2 = i // next starting index

    // 5. 建立新节点 key 映射
    const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
    for (i = s2; i <= e2; i++) {
      const nextChild = c2[i]
      if (nextChild.key != null) {
        keyToNewIndexMap.set(nextChild.key, i)
      }
    }

    // 6. 最长递增子序列优化
    let j = 0
    let patched = 0
    const toBePatched = e2 - s2 + 1
    let moved = false
    let maxNewIndexSoFar = 0
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0)

    // ...中间处理逻辑...

    // 7. 计算最长递增子序列(LIS)
    const increasingNewIndexSequence = moved
      ? getSequence(newIndexToOldIndexMap)
      : EMPTY_ARR
    j = increasingNewIndexSequence.length - 1

    // 8. 移动/新增节点
    for (i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex]
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor
      if (newIndexToOldIndexMap[i] === 0) {
        // 新增节点
        patch(null, nextChild, container, anchor, /*...*/)
      } else {
        if (moved) {
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            // 移动节点
            hostInsert(nextChild.el!, container, anchor)
          } else {
            j--
          }
        }
      }
    }
  }
}

三、关键优化点解析

1. 双端对比策略
// 头部对比
while (i <= e1 && i <= e2) { /*...*/ }

// 尾部对比
while (i <= e1 && i <= e2) { /*...*/ }
  • 时间复杂度:O(n) 快速跳过首尾相同节点
2. 最长递增子序列(LIS)
const getSequence = (arr: number[]): number[] => {
  // ...使用贪心+二分查找算法实现...
}
  • 作用:找到不需要移动的最长连续节点序列
  • 复杂度:O(n log n)
3. Key映射优化
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
  keyToNewIndexMap.set(c2[i].key, i)
}
  • 优势:O(1) 时间复杂度查找节点位置

四、性能对比(Vue2 vs Vue3)

场景Vue2 双端对比Vue3 快速路径 + LIS
纯头部插入O(n)O(1)
尾部追加O(n)O(1)
随机插入O(n)O(n)
相同节点乱序O(n^2)O(n log n)
静态节点提升O(0)

五、调试技巧

// 在浏览器控制台观察虚拟DOM结构
const app = createApp({/*...*/})
app.config.globalProperties.$logVNode = (vnode) => {
  console.log(JSON.stringify(vnode, (k, v) => {
    if (k === 'el') return undefined // 过滤DOM元素
    return v
  }, 2))
}

// 组件内使用
this.$logVNode(this._vnode)

六、性能优化实践

<!-- 1. 使用稳定的key -->
<div v-for="item in list" :key="item.id">

<!-- 2. 避免索引作为key -->
<!-- Bad -->
<div v-for="(item, index) in list" :key="index">

<!-- 3. 使用v-show替代v-if -->
<div v-show="shouldShow">

<!-- 4. 冻结静态数据 -->
<script>
const staticList = Object.freeze([/*...*/])
</script>

通过源码分析可以看出,Vue3 的 diff 算法通过以下策略实现性能突破:

  1. 双端预处理快速处理头尾变更
  2. Map结构实现快速节点查找
  3. 最长递增子序列减少DOM移动操作
  4. 静态提升跳过静态节点比对
  5. 补丁标志实现快速路径判断
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值