Vue 3 的 DOM diff 算法

Vue 3 的 DOM diff 算法(称为 快速 Diff 算法)相比 Vue 2 有了显著优化,主要特点是通过更高效的比较策略减少不必要的 DOM 操作。

1. 核心 Diff 流程

1.1 整体流程

function patchChildren(n1, n2, container) {
  // 1. 预处理:处理文本、空节点等简单情况
  // 2. 核心 Diff:比较新旧子节点数组
  // 3. 更新 DOM
}

2. 快速 Diff 算法步骤

2.1 预处理阶段

function patchKeyedChildren(c1, c2, container) {
  let i = 0
  const l2 = c2.length
  let e1 = c1.length - 1
  let e2 = l2 - 1
  
  // 1. 从前面开始同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container) // 更新节点
    } else {
      break
    }
    i++
  }
  
  // 2. 从后面开始同步
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container)
    } else {
      break
    }
    e1--
    e2--
  }
  
  // 3. 处理新增/删除情况
  if (i > e1) {
    // 新增节点
    if (i <= e2) {
      const nextPos = e2 + 1
      const anchor = nextPos < l2 ? c2[nextPos].el : null
      while (i <= e2) {
        patch(null, c2[i], container, anchor)
        i++
      }
    }
  } else if (i > e2) {
    // 删除节点
    while (i <= e1) {
      unmount(c1[i])
      i++
    }
  } else {
    // 4. 复杂情况:乱序节点比较
    const s1 = i
    const s2 = i
    const keyToNewIndexMap = new Map()
    
    // 建立新节点 key 到 index 的映射
    for (let i = s2; i <= e2; i++) {
      keyToNewIndexMap.set(c2[i].key, i)
    }
    
    // 遍历旧节点,找出可复用的节点 【新节点在旧节点的位置,后面可以直接取】
    const toBePatched = e2 - s2 + 1
    const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
    
    for (let i = s1; i <= e1; i++) {
      const prevChild = c1[i]
      let newIndex
      if (prevChild.key != null) {
        newIndex = keyToNewIndexMap.get(prevChild.key)
      } else {
        // 没有 key 的情况,需要遍历查找
        for (let j = s2; j <= e2; j++) {
          if (isSameVNodeType(prevChild, c2[j])) {
            newIndex = j
            break
          }
        }
      }
      
      if (newIndex === undefined) {
        // 新节点中没有,卸载
        unmount(prevChild)
      } else {
        newIndexToOldIndexMap[newIndex - s2] = i + 1
        patch(prevChild, c2[newIndex], container)
      }
    }
    
    // 5. 移动和挂载新节点
    const increasingNewIndexSequence = getSequence(newIndexToOldIndexMap)
    let j = increasingNewIndexSequence.length - 1
    
    for (let i = toBePatched - 1; i >= 0; i--) {
      const nextIndex = s2 + i
      const nextChild = c2[nextIndex]
      const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : null
      
      if (newIndexToOldIndexMap[i] === 0) {
        // 新增节点
        patch(null, nextChild, container, anchor)
      } else {
        if (i !== increasingNewIndexSequence[j]) {
          // 需要移动
          insert(nextChild.el, container, anchor)
        } else {
          j--
        }
      }
    }
  }
}

3. 具体示例分析

3.1 简单例子:前后同步

// 旧节点: [A, B, C, D]
// 新节点: [A, B, E, D]

// 步骤:
// 1. 前同步:比较 A-A, B-B ✓
// 2. 后同步:比较 D-D ✓  
// 3. 中间部分:C -> E,替换操作

3.2 复杂例子:乱序移动

// 旧节点: [A, B, C, D, E, F]
// 新节点: [A, E, C, B, D, F]

// 步骤:
// 1. 前同步:A-A ✓
// 2. 后同步:F-F ✓
// 3. 中间部分 [B, C, D, E] vs [E, C, B, D] 需要复杂 Diff

// 建立映射:
// keyToNewIndexMap: {E:1, C:2, B:3, D:4} // 新节点映射表
// newIndexToOldIndexMap: [4, 2, 1, 3]  // 新节点在旧节点位置
// 最长递增子序列: [2, 3] → [C, D]

// 移动操作:
// - B 需要移动到 D 前面
// - C, D 保持不动

4. 最长递增子序列算法

// 获取最长递增子序列
function getSequence(arr) {
  const p = arr.slice()
  const result = [0]
  let i, j, u, v, c
  const len = arr.length
  
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      if (arr[j] < arrI) {
        p[i] = j
        result.push(i)
        continue
      }
      
      u = 0
      v = result.length - 1
      while (u < v) {
        c = (u + v) >> 1
        if (arr[result[c]] < arrI) {
          u = c + 1
        } else {
          v = c
        }
      }
      
      if (arrI < arr[result[u]]) {
        if (u > 0) {
          p[i] = result[u - 1]
        }
        result[u] = i
      }
    }
  }
  
  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  
  return result
}

5. 性能优化策略

5.1 静态提升

// 静态节点在编译阶段被提升,不参与 Diff
const hoisted = createVNode('div', null, 'static content')

function render() {
  return createVNode('div', null, [
    hoisted, // 静态节点,直接复用
    createVNode('p', null, state.dynamicContent) // 动态节点
  ])
}

5.2 Patch Flags

// 编译时标记节点更新类型
const vnode = {
  type: 'div',
  patchFlag: 1, // TEXT - 只需要更新文本内容
  children: dynamicText
}

// Patch Flags 类型:
// 1: TEXT - 动态文本
// 2: CLASS - 动态 class
// 4: STYLE - 动态 style
// 8: PROPS - 动态属性
// 16: FULL_PROPS - 需要完整 Diff

5.3 Block Tree

// 区块树,减少动态节点比较范围
function render() {
  return openBlock(), createBlock('div', null, [
    createVNode('p', null, '静态节点'), // 不参与 Diff
    createVNode('p', null, state.dynamic, 1) // 只有这个参与 Diff
  ])
}

6. 与 Vue 2 Diff 的对比

特性 Vue 2 Vue 3
算法 双端 Diff 快速 Diff + 最长递增子序列
时间复杂度 O(n) O(n)
实际性能 较多 DOM 移动 最少 DOM 移动
静态优化 有限 静态提升、Patch Flags
编译时优化 较少 Block Tree、Tree Flattening

7. 实际应用示例

// 列表渲染优化
const List = {
  setup() {
    const items = ref([{id: 1, name: 'A'}, {id: 2, name: 'B'}, {id: 3, name: 'C'}])
    
    const reverse = () => {
      items.value = items.value.reverse()
    }
    
    return { items, reverse }
  },
  render() {
    return [
      h('button', { onClick: this.reverse }, '反转'),
      h('ul', null, 
        this.items.map(item => 
          h('li', { key: item.id }, item.name)
        )
      )
    ]
  }
}

总结

Vue 3 的 DOM Diff 主要优化点:

  1. 预处理:前后同步比较跳过相同前缀后缀
  2. 处理仅新增和仅删除
  3. Key 映射:建立 key-index 映射快速查找 【新节点映射表,新节点在旧节点位置】
  4. 最长递增子序列:找出最少移动的节点序列
  5. 编译时优化:静态提升、Patch Flags 减少运行时比较
  6. Block Tree:缩小 Diff 范围

这些优化使得 Vue 3 在复杂列表更新场景下性能显著提升,特别是在节点移动频繁的情况下。

posted @ 2025-10-13 16:56  阿木隆1237  阅读(48)  评论(0)    收藏  举报