vue3源码阅读 diff算法

引言

vue在patchChild的时候,分为4种情况,新节点、老节点分别为text、array中的一种,textToText可以删除老节点,直接设置新节点textToArray可以删除老节点,然后直接插入新节点,arrayToText可以遍历删除老节点,插入新节点,arrayToArray,则是采用的diff,当时暴力解也可以直接遍历删除老节点,然后遍历插入新节点,但是dom的操作对于js操作性能消耗更大,所以我们需要利用diff来尽可能减少dom操作

正文

vue采用的是双端对比,共设置了三个指针,如下图:

i 代表的是起始指针,新节点、老节点公用,默认是 0

e1 代表的是老节点的末尾指针, 默认是 数组最后一位数据下标

e2 代表的是新节点的末尾指针, 默认是 数组最后一位数据下标

因为vue的更新是全量的,所以并不是说我们更新了那块的数据就去对比那块的内容所以再更新时,我们首先需要找到不需要更新的地方,然后跳过

首先对比新、旧节点左侧内容

    while (i <= e1 && i <= e2) {
      const n1 = c1[i]
      const n2 = (c2[i] = optimized
        ? cloneIfMounted(c2[i] as VNode)
        : normalizeVNode(c2[i]))
      if (isSameVNodeType(n1, n2)) {
        patch(
          n1,
          n2,
          container,
          null,
          parentComponent,
          parentSuspense,
          isSVG,
          slotScopeIds,
          optimized
        )
      } else {
        break
      }
      i++
    }

对比完左侧之后,i指针向右移动二格,如下图

对比完左侧之后,同理开始对比右侧

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

对比完左侧之后,e1,e2指针向左移动二格,如下图:

然后开始比较中间部分,中间也分为三种情况,①首先先判断一下,老节点是否比较完了,如果老节点已经比较完了,由于我们最后的dom解构是基于新的节点,所以把最新的节点全部插入即可

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,
            slotScopeIds,
            optimized
          )
          i++
        }
      }
    }

②跟上面的相反,如果老的没比较完,但新的节点已经比较完了,我们直接根据老节点剩余没比较完的内容删除dom即可

    else if (i > e2) {
      while (i <= e1) {
        unmount(c1[i], parentComponent, parentSuspense, true)
        i++
      }
    }

③如果没有命中上面2种情况,则老的节点和新的节点都存再未比较的节点,

然后首先根据新节点剩余内容,根绝key创建一个map映射

 const s1 = i // prev starting index
      const s2 = i // next starting index

      // 5.1 build key:index map for newChildren
      const keyToNewIndexMap: Map<string | number | symbol, number> = new Map()
      for (i = s2; i <= e2; i++) {
        const nextChild = (c2[i] = optimized
          ? cloneIfMounted(c2[i] as VNode)
          : normalizeVNode(c2[i]))
        if (nextChild.key != null) {
          if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
            warn(
              `Duplicate keys found during update:`,
              JSON.stringify(nextChild.key),
              `Make sure keys are unique.`
            )
          }
          keyToNewIndexMap.set(nextChild.key, i)
        }
      }

后续在遍历老节点数组

 let j
      let patched = 0
      //新节点数组剩余未比较节点长度
      const toBePatched = e2 - s2 + 1
      //是否存在需要移动的节点,用于后续判断是否需要计算最长递增子序列
      let moved = false
      //记录比较节点中最大的索引
      let maxNewIndexSoFar = 0
      //记录新的节点再老节点中的索引,不存在为-1
      const newIndexToOldIndexMap = new Array(toBePatched)
      for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0

      for (i = s1; i <= e1; i++) {
        const prevChild = c1[i]
        //patched代表已经遍历过的节点个数,如果大于等于需要遍历的节点,代表着所需节点已
        //处理完成,剩余节点删除即可
        if (patched >= toBePatched) {
          unmount(prevChild, parentComponent, parentSuspense, true)
          continue
        }
        let newIndex
        //找到老节点数组此次遍历的这个节点在新节点的位置,先去通过key找,找不到再遍历新节点数组,用isSameVNodeType函数匹配
        if (prevChild.key != null) {
          newIndex = keyToNewIndexMap.get(prevChild.key)
        } else {
          for (j = s2; j <= e2; j++) {
            if (
              newIndexToOldIndexMap[j - s2] === 0 &&
              isSameVNodeType(prevChild, c2[j] as VNode)
            ) {
              newIndex = j
              break
            }
          }
        }
        //如果没找到,代表新节点数组里不存在这个节点,但是在老节点存在,即真实dom存在,就把这个节点删了
        //如果找到了,则调用patch函数,给新节点数组上的这个节点一些属性赋值,比如el之类的,后续才能根绝新节点内容做处理
        if (newIndex === undefined) {
          unmount(prevChild, parentComponent, parentSuspense, true)
        } else {
          newIndexToOldIndexMap[newIndex - s2] = i + 1
          if (newIndex >= maxNewIndexSoFar) {
            maxNewIndexSoFar = newIndex
          } else {
            moved = true
          }
          patch(
            prevChild,
            c2[newIndex] as VNode,
            container,
            null,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
          patched++
        }
      }

然后遍历新节点数组剩余未遍历节点

 //根据moved判断是否需要求出最长递增子序列
      //最长递增子序列可以保证这个子序列里面的内容的相对位置不变,这样可以尽量减少dom的移动
      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) {
          // mount new
          patch(
            null,
            nextChild,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            slotScopeIds,
            optimized
          )
        } else if (moved) {
          因为是倒叙,所以直接和最长递增子序列最后一个元素匹配即可,匹配不上,则需要移动,匹配上了,直接跳过
          if (j < 0 || i !== increasingNewIndexSequence[j]) {
            move(nextChild, container, anchor, MoveType.REORDER)
          } else {
            j--
          }
        }
      }

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值