react源码分析(6)-DIFF算法介绍

DIFF算法总体介绍

由于将树转化为另一种树需要大量时间,事实上,算法的时间复杂度为O(n^3),所以必须要想一种方法进行替代,这就是DIFF算法。

首先研究实际开发中的情况,第一,很少有在更新时节点跨层级移动的情况,即使有,也可以用CSS进行隐藏或消失。第二,当两个组件的类不相同时,它们通常不会有相同的结构,第三,在同层级的节点比较中,可以以key做唯一标识。

根据第一条,DIFF算法可以只比较同层级节点,从而将算法的时间复杂度降为O(n)。根据第二条,算法将不再对不同类型的节点进行虚拟DOM对比,而是直接替换。根据第三条,可以直接在同层级节点中进行删除,增添与移动,移动时根据key进行区分

这里讲一下节点的移动,lastIndex代指更新完成的最后一个元素,同层级节点遍历新树时,先寻找是否有相应的新节点,如果有就使用,但注意,如果index小于lastIndex,那么需要移动至相应的位置并且更新lastIndex,如果大于或等于,就不需要移动。(这里只是简单一提,在下文的源码部分会有更加详细的描述

源码解析部分

大致介绍了DIFF算法。在这里大致看一下源码部分,以子节点为数组时举例

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
  ): Fiber | null {
    // This algorithm can't optimize by searching from both ends since we
    // don't have backpointers on fibers. I'm trying to see how far we can get
    // with that model. If it ends up not being worth the tradeoffs, we can
    // add it later.

    // Even with a two ended optimization, we'd want to optimize for the case
    // where there are few changes and brute force the comparison instead of
    // going for the Map. It'd like to explore hitting that path first in
    // forward-only mode and only go for the Map once we notice that we need
    // lots of look ahead. This doesn't handle reversal as well as two ended
    // search but that's unusual. Besides, for the two ended optimization to
    // work on Iterables, we'd need to copy the whole set.

    // In this first iteration, we'll just live with hitting the bad case
    // (adding everything to a Map) in for every insert/move.

    // If you change this code, also update reconcileChildrenIterator() which
    // uses the same algorithm.

    

    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        // TODO: This breaks on empty slots like null children. That's
        // unfortunate because it triggers the slow path all the time. We need
        // a better way to communicate whether this was a miss or null,
        // boolean, undefined, etc.
        if (oldFiber === null) {
          oldFiber = nextOldFiber;
        }
        break;
      }
      if (shouldTrackSideEffects) {
        if (oldFiber && newFiber.alternate === null) {
          // We matched the slot, but we didn't reuse the existing fiber, so we
          // need to delete the existing child.
          deleteChild(returnFiber, oldFiber);
        }
      }
      lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
      if (previousNewFiber === null) {
        // TODO: Move out of the loop. This only happens for the first run.
        resultingFirstChild = newFiber;
      } else {
        // TODO: Defer siblings if we're not at the right index for this slot.
        // I.e. if we had null values before, then we want to defer this
        // for each null value. However, we also don't want to call updateSlot
        // with the previous one.
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }

    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }

    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx], lanes);
        if (newFiber === null) {
          continue;
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
      return resultingFirstChild;
    }

    // Add all children to a key map for quick lookups.
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          if (newFiber.alternate !== null) {
            // The new fiber is a work in progress, but if there exists a
            // current, that means that we reused the fiber. We need to delete
            // it from the child list so that we don't add it to the deletion
            // list.
            existingChildren.delete(
              newFiber.key === null ? newIdx : newFiber.key,
            );
          }
        }
        lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }
    }

    if (shouldTrackSideEffects) {
      // Any existing children that weren't consumed above were deleted. We need
      // to add them to the deletion list.
      existingChildren.forEach(child => deleteChild(returnFiber, child));
    }

    return resultingFirstChild;
  }

 

在这里给出解释,首先是方法中的四个参数,returnFiber为Fiber的父节点,而currentFiberChild就是现在Fiber节点的child,newChildren为当前新数组,而lanes则与优先级有关(Fiber任务调度)。

接下来说一下遍历过程,首先进行第一遍循环,比较的值为oldFiber与newChildren[i],通过updateSlot进行判断,如果两者的key与类型均相同,那么就进行元素复用,生成一个新Fiber,而且i++,oldFiber为oldFiber.sibling,假如无法复用,则新Fiber为null,跳出循环。当跳出循环时,共有三种情况,假如为newChildren.length,那么在此时新节点已经遍历完成,可以删除其余节点。如果olderFiber为null,那就将剩余节点全部插入。如果新节点与旧节点均未遍历结束,那么就是出现了无法复用的节点(我认为并不完全是移动的情况,假如旧节点为abcde,新节点为abcdf,那么同样由第三种情况处理,但并不是移动),现在我们可以回到上文中所提到的移动的逻辑,将所有旧节点设立为map,然后根据key值依次查询新节点是否可以从其中复用,假如可以复用,就将新节点的alternate属性设置为旧节点,并将其从map中删除,如果无法复用,则将alternate属性设置为空。然后进行placeChild,placeChild中oldIndex代表旧节点的位置,而lastPlaceIndex为最近更新节点的索引,假如新节点中的alternate为空,那么执行插入方法,假如oldIndex小于lastIndex,那么进行移动,上述两种情况的lastPlaceIndex不会发生变化,而如果oldIndex大于等于lastIndex,那么不会移动节点,而是将lastPlaceIndex更新为oldIndex(此处移动的逻辑已经在上文的总体介绍中说过,这里详细解释),然后将map中剩余节点删除即可。

总结

这里结合我的react源码分析(3)-Fiber节点,树的介绍以及渲染过程总览中对Fiber节点的相关内容讲解了react需要重新渲染时的DIFF算法的原理以及DIFF算法下Fiber节点的插入删除与更新。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值