React diff算法解析

543 阅读4分钟

大体流程

1 按层对比,如果不相同则直接重新创建
2 如果是数组,则对比前后形成的数组,按照一定的规则对比

数组diff

1 首先遍历新数组,如果旧数组和新数组的index相同的情况下,如果key相同,则利用旧数组的元素,形成的新的元素。如果key不同,则直接跳出循环,到第4步
2 如果新数组遍历完,还有老数组的元素存在,则删除
3 如果老数组遍历结束之后,新数组还存在,则直接把新数组的元素都插入
4 开始第二次循环,首先把老数组中的元素按照key形成hash表和一个exist数组,遍历新数组,如果新数组中的元素的key在hash表存在的话,就利用hash表中对应的值形成新的元素,同时在exist数组中删除
5 结果得到一个打上各种tag的数组

// returnFiber表示父元素fiber,currentFirstChild 表示以前的数组中的第一个元素
  // newChildren 表示当前形成的新数组,expirationTime 时间片
  function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren, expirationTime) {
    // 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.

    {
      // First, validate keys.
      var knownKeys = null;
      for (var i = 0; i < newChildren.length; i++) {
        var child = newChildren[i];
        knownKeys = warnOnInvalidKey(child, knownKeys);
      }
    }

    var resultingFirstChild = null;
    var previousNewFiber = null;

    var oldFiber = currentFirstChild;
    var lastPlacedIndex = 0;
    var newIdx = 0;
    var nextOldFiber = null;
    // 这里的oldFiber是指在旧的数组中的第一个元素,首先依次遍历新数组,首先看index相同的元素是否key也相同
    // 如果相同,就使用老元素生成新元素
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      // 暂时没发现什么情况下出现这种情况
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      // 如果key相同,则根据oldFiber 生成 newFiber
      var newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], expirationTime);
      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表示旧数组中的所有已经在新数组中更新好了的元素的最大的位置,所以数组
      // 中如果有元素的索引小于lastPlacedIndex,所以该元素在新数组中的索引肯定和旧数组中是不一样的,肯定是需要
      // 移动顺序的。
      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++) {
        var _newFiber = createChild(returnFiber, newChildren[newIdx], expirationTime);
        if (!_newFiber) {
          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.
    // 把老数组中按照key形成hash表
    var existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    // 然后再新数组中查找,如果新数组中的key在老数组中就利用老数组中的元素,更新新数组中的元素
    for (; newIdx < newChildren.length; newIdx++) {
      var _newFiber2 = updateFromMap(existingChildren, returnFiber, newIdx, newChildren[newIdx], expirationTime);
      if (_newFiber2) {
        if (shouldTrackSideEffects) {
          if (_newFiber2.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(_newFiber2.key === null ? newIdx : _newFiber2.key);
          }
        }
        lastPlacedIndex = placeChild(_newFiber2, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          resultingFirstChild = _newFiber2;
        } else {
          previousNewFiber.sibling = _newFiber2;
        }
        previousNewFiber = _newFiber2;
      }
    }

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

    return resultingFirstChild;
  }

例子

1 [A,B,C,D,E] =>[A,F,B,C,D] 步骤
1 首先遍历新数组,如果旧数组和新数组的index相同的情况下,如果key相同,则利用旧数组的元素,形成的新的元素。如果key不同,则直接跳出循环,到第4步
这里到看到B和F两者的key是不一样的,所以跳出循环,到第4步 2 如果新数组遍历完,还有老数组的元素存在,则删除
3 如果老数组遍历结束之后,新数组还存在,则直接把新数组的元素都插入
4 开始第二次循环,首先把老数组中的元素按照key形成hash表和一个exist数组,遍历新数组,如果新数组中的元素的key在hash表存在的话,就利用hash表中对应的值形成新的元素,同时在exist数组中删除
4.1 hash = { B:B, C:C, D,D, E,E }
exit = [B,C,D,E]
lastPlaceIndex = 0;lastPlaceIndex是指在旧数组中已经更新到新数组中元素中索引最大的值,所以这里是0
oldIndex=undefined,oldIndex是指当前旧数组中遍历到元素的索引
newIndex=1,newIndex是指当前新数组中遍历到元素的索引
4.2 newIndex = 1;新数组中是F,可以发现在hash中不存在,则打上插入的tag,lastPlaceIndex=0不变
4.3 newIndex =2,新数组中是B,在hash中存在,则oldIndex=1,因为oldIndex > lastPlaceIndex,则不发生变化,同时lastPlaceIndex =1,同时exit=[C,D,E]
4.4 newIndex =3,新数组中是C,在hash中存在,则oldIndex=2,因为oldIndex > lastPlaceIndex,则不发生变化,同时lastPlaceIndex =2,同时exit=[D,E]
4.5 依次类推,最后直接删除E

5 结果得到一个打上各种tag的数组