vue diff算法

63 阅读4分钟

vue2 diff

前置

  • 节点
    • 旧列表的第一个节点表示为oldStartNode
    • 旧列表的最后一个节点表示为oldEndNode
    • 新列表的第一个节点表示为newStartNode
    • 新列表的最后一个节点表示为newEndNode
  • 下标
    • 旧列表的头指针表示为oldStartIndex
    • 旧列表的尾指针表示为oldEndIndex
    • 新列表的头指针表示为newStartIndex
    • 新列表的尾指针表示为newEndIndex

第一轮

  • 使用oldStartNodenewStartNode对比
    • 相同,oldStartIndexnewStartIndex,同时向移动一位,元素无需移动
  • 使用oldEndNodenewEndNode对比
    • 相同,oldEndIndexnewEndIndex,同时向移动一位,元素无需移动
  • 使用oldStartNodenewEndNode对比
    • 相同,oldStartIndex移动一位,newEndIndex移动一位,oldStartNode移动到原本尾节点的后面
  • 使用oldEndNodenewStartNode对比
    • 相同,oldEndIndex移动一位newStartIndex移动一位,oldEndNode移动到原本头节点的前面

第二轮

  • 当四次对比都没找到复用节点时,我们拿新列表的第一个节点去旧列表中找与其key相同的节点
    • 找到:节点移动oldStartIndex之前,旧列表中的节点改为undefinedoldStartIndex后移
    • 没找到:直接创建一个新的节点放到最前面就可以了,newStartIndex后移

终止条件

  • oldStartIndex 大于 oldEndIndex,但是新列表中还有剩余的节点,我们只需要将剩余的节点依次插入到oldStartNodeDOM之前
  • newEndIndex小于newStartIndex时,我们将旧列表剩余的节点删除

vue3 diff

前置

  • 节点
    • 旧列表的第一个节点表示为oldStartNode
    • 旧列表的最后一个节点表示为oldEndNode
    • 新列表的第一个节点表示为newStartNode
    • 新列表的最后一个节点表示为newEndNode
  • 下标
    • 旧列表的头指针表示为oldStartIndex
    • 旧列表的尾指针表示为oldEndIndex
    • 新列表的头指针表示为newStartIndex
    • 新列表的尾指针表示为newEndIndex

第一轮

  • 使用oldStartNodenewStartNode对比
    • 相同,oldStartIndexnewStartIndex,同时向移动一位,元素无需移动
    • 不相同,停止
  • 使用oldEndNodenewEndNode对比
    • 相同,oldEndIndexnewEndIndex,同时向移动一位,元素无需移动
    • 不相同,停止

第二轮

  • 这时会出现四种场景
    • 旧列表遍历结束,新列表遍历结束:不需要第二轮遍历,对应 位置无变化
    • 旧列表有剩余节点,新列表遍历结束:老节点全部删除
    • 旧列表遍历结束,新列表有剩余节点:新节点全部插入
    • 旧列表有剩余节点,新列表有剩余节点,进入diff

diff

  • 以剩余的新节点为映射数组,并生成一个映射数组等长的下标数组,与遍历剩余的老节点,看老节点是否在映射数组
    • 存在:给映射数组对应下标数组写入老节点的下标
    • 不存在:老节点需要删除
    • 注意下标数组默认都是-1,-1代表新节点需要新增
  • 在遍历时也会比较当前节点下标是否大于上一个节点
    • 如果大于则不需要移动
      • 需要判断是否有全新的节点,如果有给添加进去
    • 如果小于则需要移动
      • 下标数组使用最长递增子序列算法(下面会说)算出最长递增的数组
      • 遍历下标数组,并在最长递增的数组中查找
        • 存在:不需要移动
        • 不存在:新建节点插入到前一个vnode节点之后
          • 为什么是这样移动的呢?
            • 首先列表是从头到尾遍历的。这就意味着对于当前VNode节点来说,该节点之前的所有节点都是排好序的,如果该节点需要移动,那么只需要将DOM节点移动到前一个vnode节点之后就可以

最长递增子序列算法

  • 实现了两种,vue3源码中的类似于第二种
function getSequence(arr) {
  const dp = new Array(arr.length).fill(1)
  for (let i = 1; i < arr.length; i++) {
    for (let j = i - 1; j >= 0; j--) {
      if (arr[i] > arr[j]) dp[i] = Math.max(dp[i], dp[j] + 1)
    }
  }
  let max = Math.max(...dp)
  const result = []
  for (let i = arr.length - 1; max > 0; i--) {
    if (dp[i] == max) {
      result.unshift(arr[i])
      max--
    }
  }
  return result
}
function getSequence(arr) {
  const p = arr.slice() // 记录递增子序列前一位的下标
  const result = [0] // 记录递增的下标
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] > arr[result[result.length - 1]]) {
      p[i] = result[result.length - 1]
      result.push(i)
    } else {
      // 二分查找
      let l = 0
      let r = result.length - 1
      let m = Math.floor((l + r) / 2) | 0
      while (l < r) {
        if (arr[result[m]] < arr[i]) {
          l = m + 1
        } else {
          r = m - 1
        }
        m = Math.floor((l + r) / 2)
      }
      result[l] = i
      p[i] = result[l - 1]
    }
  }

  // result 不一定是正确的最长递增序列,中间有些数有可能被替换了
  // 倒序遍历 result,根据 p 替换前面的数
  let len = result.length
  let max = result[len - 1]
  while (len > 0) {
    len--
    result[len] = max
    max = p[max]
  }
  return result
}