今天我们来接着说diff算法?

1、patchChildren
前面我们提到了存在children的vnode,那么存在children就需要patch每一个children vnode向下遍历,我们这里就需要用到patchChildren,依次patch子类的vnode。
vue3.0中有这么一段源码

if (patchFlag > 0) {
      if (patchFlag & PatchFlags.KEYED_FRAGMENT) { 
         /* 对于存在key的情况用于diff算法 */
        patchKeyedChildren(
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      } else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
         /* 对于不存在key的情况,直接patch  */
        patchUnkeyedChildren( 
          c1 as VNode[],
          c2 as VNodeArrayChildren,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
        return
      }
    }

patchChildren会根据key是否存在进行真正的diff或者直接patch
那么我们看既然diff存在patchChildren方法中,patchChildren方法用在Fragment类型和element类型的vnode中,这样就解释了diff算法的作用域。

diff算法的作用
存在children的vnode,需要通过patchChildren遍历children依次进行patch操作,如果在patch期间再发下children,那么会再次以递归的方式向下patch,因此找到新旧vnode很重要。
这里我们可以设想下,如果不存在diff,依次按照先后顺序进行patch会怎么样?
如果没有diff算法,如果稍微调整下DOM结构,就不会有新老节点的复用,就需要重新创建新的vnode节点,这样浪费了很大的性能开销。

diff作用就是在patch子vnode过程中,找到与新vnode对应的老vnode,复用真实的dom节点,避免不必要的性能开销

2、diff算法做了什么?
上面的源码中我们提到了key与没有key,源码中有不同的方法patchKeyedChildrenpatchUnkeyedChildren
首先我们看看不存在key

c1 = c1 || EMPTY_ARR
    c2 = c2 || EMPTY_ARR
    const oldLength = c1.length
    const newLength = c2.length
    const commonLength = Math.min(oldLength, newLength)
    let i
    for (i = 0; i < commonLength; i++) { /* 依次遍历新老vnode进行patch */
      const nextChild = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      patch(
        c1[i],
        nextChild,
        container,
        null,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized
      )
    }
    if (oldLength > newLength) { /* 老vnode 数量大于新的vnode,删除多余的节点 */
      unmountChildren(c1, parentComponent, parentSuspense, true, commonLength)
    } else { /* /* 老vnode 数量小于于新的vnode,创造新的即可 */
      mountChildren(
        c2,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        optimized,
        commonLength
      )
    }

以上得出结论:
(1)比较新老children的length获取最小值 然后对于公共部分,进行从新patch工作;
(2)如果老节点数量大于新的节点数量 ,删除多出来的节点;
(3)如果新的节点数量大于老节点的数量,从新 mountChildren新增的节点(也就是创建新的节点)。

下面是有key的方法 patchKeyedChildren

/* 从头对比找到有相同的节点 patch ,发现不同,立即跳出*/
    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
        /* 判断key ,type是否相等 */
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container, 
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      i++
    }

以上得出结论
(1)从头开始寻找相同的vnode,然后patch,如果发现不相同就跳出循环。

// isSameVNodeType
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
  return n1.type === n2.type && n1.key === n2.key
}

isSameVNodeType 作用就是判断当前vnode类型 和 vnode的 key是否相等

(2)从尾开始向前diff
如果第一步没有patch完,就从尾开始向前diff,发现相同就patch,不同立即跳出循环

 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,
          parentAnchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      } else {
        break
      }
      e1--
      e2--
    }

(3)看看老节点是否全部被patch,新节点没有被patch完,创建新的vnode,

/* 如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode  ) */
    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,
            isSVG
          )
          i++
        }
      }
    }
    
//如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode ),也就是要全部create新的vnode.

(4)如果新节点全部被patch完毕,老节点仍有剩余,那么卸载所有超出的老节点。

else if (i > e2) {
   while (i <= e1) {
      unmount(c1[i], parentComponent, parentSuspense, true)
      i++
   }
}
//针对老节点多余新节点,老节点全部被卸载的情况说明已经patch完相同的vnode。

(5)不确定的元素(这说明没有patch完所有的相同的vnode)
然后我们回到第一步跟第二步,看看剩余节点怎么操作如下:

const s1 = i  //第一步遍历到的index
const s2 = i 
const keyToNewIndexMap: Map<string | number, number> = new Map()
/* 把没有比较过的新的vnode节点,通过map保存 */
for (i = s2; i <= e2; i++) {
  if (nextChild.key != null) {
    keyToNewIndexMap.set(nextChild.key, i)
  }
}
let j
let patched = 0 
const toBePatched = e2 - s2 + 1 /* 没有经过 path 新的节点的数量 */
let moved = false /* 证明是否 */
let maxNewIndexSoFar = 0 
const newIndexToOldIndexMap = new Array(toBePatched)
 for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
/* 建立一个数组,每个子元素都是0 [ 0, 0, 0, 0, 0, 0, ] */ 

遍历所有新节点把索引和对应的key,存入map keyToNewIndexMap中,keyToNewIndexMap 存放 key -> index 的map
接下来声明一个新的指针,记录剩下新节点的索引,我们可以看到这个patched记录新节点过的数量。
toBePatched记录第五步之前没有patched的新节点的数量
moved代表是否发生过移动
newIndexToOldIndexMap 用来存放新节点索引和老节点索引的数组
newIndexToOldIndexMap 数组的index是新vnode的索引 , value是老vnode的索引
然后我们看下核心代码

for (i = s1; i <= e1; i++) { /* 开始遍历老节点 */
        const prevChild = c1[i]
        if (patched >= toBePatched) { /* 已经patch数量大于等于, */
          /* ① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点 */
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
         /* ② 如果,老节点的key存在 ,通过key找到对应的index */
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else { /*  ③ 如果,老节点的key不存在 */
          for (j = s2; j <= e2; j++) { /* 遍历剩下的所有新节点 */
            if (
              newIndexToOldIndexMap[j - s2] === 0 && /* newIndexToOldIndexMap[j - s2] === 0 新节点没有被patch */
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) { /* 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex  */
              newIndex = j
              break
            }
          }
        }
        if (newIndex === undefined) { /* ①没有找到与老节点对应的新节点,删除当前节点,卸载所有的节点 */
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          /* ②把老节点的索引,记录在存放新节点的数组中, */
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            /* 证明有节点已经移动了   */
            moved = true
          }
          /* 找到新的节点进行patch节点 */
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
          patched++
        }
 }

① 通过老节点的key找到对应新节点的index:开始遍历老的节点,判断有没有key, 如果存在key通过新节点的keyToNewIndexMap找到与新节点index,如果不存在key那么会遍历剩下来的新节点试图找到对应index。
②如果存在index证明有对应的老节点,那么直接复用老节点进行patch,没有找到与老节点对应的新节点,删除当前老节点。
③newIndexToOldIndexMap找到对应新老节点关系

我们patch一遍,将所有的老的vnode都patch了一遍。

接下来,我们孙然已经patch可一遍老节点,但是对于发生移动的节点,怎么真正的moved DOM节点。
对于新增的节点又该怎么处理呢?接着往下看,下面这个方法我不是很明白getSequence

/*移动老节点创建新节点*/
     /* 根据最长稳定序列移动相对应的节点 */
      const increasingNewIndexSequence = moved
        ? getSequence(newIndexToOldIndexMap)
        : EMPTY_ARR
      j = increasingNewIndexSequence.length - 1
      for (i = toBePatched - 1; i >= 0; i--) {
        const nextIndex = s2 + i
        const nextChild = c2[nextIndex] as VNode
        const anchor =
          nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
        if (newIndexToOldIndexMap[i] === 0) { /* 没有老的节点与新的节点对应,则创建一个新的vnode */
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG
          )
        } else if (moved) {
          if (j < 0 || i !== increasingNewIndexSequence[j]) { /*如果没有在长*/
            /* 需要移动的vnode */
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }    

(6)说是通过getSequence的到一个最长稳定序列。对应index==0的操作也就是新增节点,需要从新mount一个新的节点,对于发生移动的节点进行一个统一的移动操作。

*最长稳定序列,不是很懂,我还得研究下
那么我们为啥要得到这个一个东东,因为我们需要他作为一个参照的序列,其他不在列的节点进行移动。

以下总结是copy的,组织语言有点麻烦,省事,如有雷同,勿喷
总结如下:
①从头对比找到有相同的节点 patch ,发现不同,立即跳出。

②如果第一步没有patch完,立即,从后往前开始patch ,如果发现不同立即跳出循环。

③如果新的节点大于老的节点数 ,对于剩下的节点全部以新的vnode处理( 这种情况说明已经patch完相同的vnode )。

④ 对于老的节点大于新的节点的情况 , 对于超出的节点全部卸载 ( 这种情况说明已经patch完相同的vnode )。

⑤不确定的元素( 这种情况说明没有patch完相同的vnode ) 与 3 ,4对立关系。

把没有比较过的新的vnode节点,通过map保存
记录已经patch的新节点的数量 patched
没有经过 path 新的节点的数量 toBePatched
建立一个数组newIndexToOldIndexMap,每个子元素都是[ 0, 0, 0, 0, 0, 0, ] 里面的数字记录老节点的索引 ,数组索引就是新节点的索引
开始遍历老节点
① 如果 toBePatched新的节点数量为0 ,那么统一卸载老的节点
② 如果,老节点的key存在 ,通过key找到对应的index
③ 如果,老节点的key不存在
1 遍历剩下的所有新节点
2 如果找到与当前老节点对应的新节点那么 ,将新节点的索引,赋值给newIndex
④ 没有找到与老节点对应的新节点,卸载当前老节点。
⑤ 如果找到与老节点对应的新节点,把老节点的索引,记录在存放新节点的数组中,
1 如果节点发生移动 记录已经移动了
2 patch新老节点 找到新的节点进行patch节点
遍历结束
如果发生移动
① 根据 newIndexToOldIndexMap 新老节点索引列表找到最长稳定序列
② 对于 newIndexToOldIndexMap -item =0 证明不存在老节点 ,从新形成新的vnode
③ 对于发生移动的节点进行移动处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值