vue3源码之diff以及最长递增子序列详解(多种方法)

终于来到了我最想写的diff了。当时看这一块代码看了好久。然后顺带get了最长上升子序列算法和vue3源码中做的优化。

新子节点数组相对于旧子节点数组的变化, 无非是通过更新, 删除, 添加和移动节点来完成的.

新旧子节点数组很容易拥有相同的头尾节点. 对于相同的节点, 我们只需要对比更新即可.

1.同步头部节点

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let i = 0
  const l2 = c2.length
  // 旧子节点的尾部索引
  let e1 = c1.length - 1
  // 新子节点的尾部索引
  let e2 = l2 - 1
  // 1. 从头部开始同步
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d
  while (i <= e1 && i <= e2) {
    const n1 = c1[i]
    const n2 = c2[i]
    if (isSameVNodeType(n1, n2)) {
      // 相同的节点,递归执行 patch 更新节点
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    else {
      break
    }
    i++
  }
}

2.同步尾部节点

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let i = 0
  const l2 = c2.length
  // 旧子节点的尾部索引
  let e1 = c1.length - 1
  // 新子节点的尾部索引
  let e2 = l2 - 1
  // 1. 从头部开始同步
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d


  // 2. 从尾部开始同步
  // i = 2, e1 = 3, e2 = 4
  // (a b) (c d)
  // (a b) e (c d)
  while (i <= e1 && i <= e2) {
    const n1 = c1[e1]
    const n2 = c2[e2]
    if (isSameVNodeType(n1, n2)) {
      patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized)
    }
    else {
      break
    }
    e1--
    e2--
  }
}

完成这两次的同步循环以后, 接下来就只有3中情况的处理了:

  1. 新子节点有剩余要添加的新节点
  2. 旧子节点有剩余要删除的多余节点
  3. 未知子序列

3.添加新的节点

首先判断新子节点是否有剩余:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let i = 0
  const l2 = c2.length
  // 旧子节点的尾部索引
  let e1 = c1.length - 1
  // 新子节点的尾部索引
  let e2 = l2 - 1
  // 1. 从头部开始同步
  // i = 0, e1 = 3, e2 = 4
  // (a b) c d
  // (a b) e c d
  // ...
  // 2. 从尾部开始同步
  // i = 2, e1 = 3, e2 = 4
  // (a b) (c d)
  // (a b) e (c d)
  // 3. 挂载剩余的新节点
  // i = 2, e1 = 1, 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)
        i++
      }
    }
  }
}

如果索引i大于尾部索引e1i小于e2,那么从索引i开始到索引e2之间,我们直接挂载新子树这部分的节点。

4.删除多余节点

如果不满足添加新节点的情况, 就要几折判断旧子节点是否有剩余, 如果满足则删除旧子节点, 实现代码如下:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
  let i = 0
  const l2 = c2.length
  // 旧子节点的尾部索引
  let e1 = c1.length - 1
  // 新子节点的尾部索引
  let e2 = l2 - 1
  // 1. 从头部开始同步
  // i = 0, e1 = 4, e2 = 3
  // (a b) c d e
  // (a b) d e
  // ...
  // 2. 从尾部开始同步
  // i = 2, e1 = 4, e2 = 3
  // (a b) c (d e)
  // (a b) (d e)
  // 3. 普通序列挂载剩余的新节点
  // i = 2, e1 = 2, e2 = 1
  // 不满足
  if (i > e1) {
  }
  // 4. 普通序列删除多余的旧节点
  // i = 2, e1 = 2, e2 = 1
  else if (i > e2) {
    while (i <= e1) {
      // 删除节点
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
    }
  }
}

5.处理未知子序列

对于即不满足新增也不满足删除的情况, 我们需要处理未知子序列

对于子节点的操作, 归结起来就是更新, 删除, 添加和移动.

  • 对于两个相同类型的节点, 我们执行更新操作,
  • 新节点没有而旧节点有, 执行删除操作.
  • 新节点有而旧节点没有, 则执行新增操作.
  • 相对比较麻烦的实际上是移动子节点

移动子节点

比如对于这样两个序列:

var prev = [1, 2, 3, 4, 5, 6]
var next = [1, 3, 2, 6, 4, 5]

思路是在next中找到最长的递增子序列; 序列越短,移动元素次数越少,代价越小。 

1.建立索引:

keyToNewIndexMap 存储的就是新子序列中每个节点在新子序列中的索引,

旧节点:     a b  c d e f g h 

新节点:   a b e  c d   i g  h

得到了一个值为 {e:2,c:3,d:4,i:5} 的新子序列索引图。

2.更新和移除旧节点

我们就需要遍历旧子序列,有相同的节点就通过 patch 更新,并且移除那些不在新子序列中的节点,同时找出是否有需要移动的节点,我们来看一下这部分逻辑的实现:

const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {

  let i = 0

  const l2 = c2.length

  // 旧子节点的尾部索引

  let e1 = c1.length - 1

  // 新子节点的尾部索引

  let e2 = l2 - 1

  // 1. 从头部开始同步

  // i = 0, e1 = 7, e2 = 7

  // (a b) c d e f g h

  // (a b) e c d i g h

  // 2. 从尾部开始同步

  // i = 2, e1 = 7, e2 = 7

  // (a b) c d e f (g h)

  // (a b) e c d i (g h)

  // 3. 普通序列挂载剩余的新节点,不满足

  // 4. 普通序列删除多余的旧节点,不满足

  // i = 2, e1 = 4, e2 = 5

  // 旧子序列开始索引,从 i 开始记录

  const s1 = i

  // 新子序列开始索引,从 i 开始记录

  const s2 = i

  // 5.1 根据 key 建立新子序列的索引图

  // 5.2 正序遍历旧子序列,找到匹配的节点更新,删除不在新子序列中的节点,判断是否有移动节点

  // 新子序列已更新节点的数量

  let patched = 0

  // 新子序列待更新节点的数量,等于新子序列的长度

  const toBePatched = e2 - s2 + 1

  // 是否存在要移动的节点

  let moved = false

  // 用于跟踪判断是否有节点移动

  let maxNewIndexSoFar = 0

  // 这个数组存储新子序列中的元素在旧子序列节点的索引,用于确定最长递增子序列

  const newIndexToOldIndexMap = new Array(toBePatched)

  // 初始化数组,每个元素的值都是 0

  // 0 是一个特殊的值,如果遍历完了仍有元素的值为 0,则说明这个新节点没有对应的旧节点

  for (i = 0; i < toBePatched; i++)

    newIndexToOldIndexMap[i] = 0

  // 正序遍历旧子序列

  for (i = s1; i <= e1; i++) {

    // 拿到每一个旧子序列节点

    const prevChild = c1[i]

    if (patched >= toBePatched) {

      // 所有新的子序列节点都已经更新,剩余的节点删除

      unmount(prevChild, parentComponent, parentSuspense, true)

      continue

    }

    // 查找旧子序列中的节点在新子序列中的索引

    let newIndex = keyToNewIndexMap.get(prevChild.key)

    if (newIndex === undefined) {

      // 找不到说明旧子序列已经不存在于新子序列中,则删除该节点

      unmount(prevChild, parentComponent, parentSuspense, true)

    }

    else {

      // 更新新子序列中的元素在旧子序列中的索引,这里加 1 偏移,是为了避免 i 为 0 的特殊情况,影响对后续最长递增子序列的求解

      newIndexToOldIndexMap[newIndex - s2] = i + 1

      // maxNewIndexSoFar 始终存储的是上次求值的 newIndex,如果不是一直递增,则说明有移动

      if (newIndex >= maxNewIndexSoFar) {

        maxNewIndexSoFar = newIndex

      }

      else {

        moved = true

      }

      // 更新新旧子序列中匹配的节点

      patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized)

      patched++

    }

  }

}

然后去倒序遍历newIndexToOldIndexMap; 如果值为0,则代表新增,如果在最长子序列里面就继续遍历;不在则新增;

为什么需要倒序遍历呢?  因为倒序遍历可以方便我们使用最后更新的节点作为锚点。????没太明白这个点。。。

最长递增子序列:

动态规划:

var arr = [2,3,5,10,7,20,100,0];
let dp =[];
for(let i=0;i<arr.length;i++){
    dp[i]=1;
}
let res = 0;
for(let i=1;i<arr.length;i++){
    for(let j=0;j<i;j++){
        if(arr[j]<arr[i]){
            dp[i] = Math.max(dp[i],dp[j]+1)  // dp[i]代表第i个位置上升子序列中元素的长度
        }
    }
    res = Math.max(res,dp[i])
}
console.log(res)

最小二分法

let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9]
let subArr = [arr[0]]
for (let i = 1; i < arr.length; i++) {
  if (arr[i] > subArr[subArr.length - 1]) {
    subArr.push(arr[i])
  } else {
    // arr[i] 较小

    let u = 0
    let c = 0
    let v = subArr.length - 1
    // 二分搜索,查找比 arrI 小的节点,更新 result 的值
    while (u < v) {
      c = ((u + v) / 2) | 0

      if (arr[i] > subArr[c]) {
        u = c + 1
      } else {
        v = c
      }
    }

    if (arr[i] < subArr[u]) {
      subArr[u] = arr[i]
    }
  }
}

对于let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9];

subArr 的历程是:1

                            1,5

                            1,5,6

                            1,5,6,7

                            1,5,6,7,8

                            1,2,6,7,8

                            1,2,3,7,8

                           1,2,3,4,8

                          1,2,3,4,8,9

虽然序列不正确,但是不影响它的长度的正确性;

这个最小二分法只能求长度;不能得到正确的子序列;

想要拿到正确的最长子序列(可能不是唯一的),就得再维护一个数组;

下面这段代码和上面完全一样,除了subArr存储的意义;上面保存的arr的值;下面的是arr的index  也是for循环里面的i(其实i更好理解一些)

let arr = [1, 5, 6, 7, 8, 2, 3, 4, 9]
let subArr = [0]
let prevIndex=[0];
for (let i = 1; i < arr.length; i++) {
  if (arr[i] > arr[subArr[subArr.length - 1]]) {
    prevIndex.push(subArr[subArr.length - 1])
    subArr.push(i)
  } else {// arr[i] 较小
    
    let u = 0;
    let c= 0;
    let v = subArr.length - 1
    // 二分搜索,查找比 arrI 小的节点,更新 result 的值
    while (u < v) {
      c = ((u + v) / 2) | 0
     
      if (arr[i] > arr[subArr[c]]) {
        u = c + 1
        
      } else {
        v = c
      }
    }
   
    if (arr[i] < arr[subArr[u]]) {
      prevIndex.push(subArr[u-1])
      subArr[u] = i
    }
  }
// 利用前驱节点重新计算subArr
 let length = subArr.length; //总长度
 let  prev = subArr[length - 1] // 最后一项
  while (length-- > 0) {// 根据前驱节点一个个向前查找
    subArr[length] = prev
    prev = prevIndex[subArr[length]]
  }
   
}

增加一个数组保存subArr更新前的index;记录result的更新过程;prevIndex数组的第一个没有意义;因为for循环是从1开始的;

prevIndex[i]记录的是subArr在arr[i]更新前记录的上一个值;

subArr最后一个值一定是准确的;当它记录进去的时候它的上一个值也一定是准确的(可能有不同的序列,但一定是某个正确的序列里面的值);

所以通过prevIndex[i]拿到它的上一个准确值;这样递归循环;

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值