• 作者:老汪软件技巧
  • 发表时间:2024-08-21 17:02
  • 浏览量:

react调和函数的作用就是在react fiber树构建过程中,比较旧的fiber和新的reactElement,尽量复用旧fiber的一个方法,也就是常说的diff算法

在reconcileChildFibers逻辑中,区分为子节点为单节点和多节点

function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // 处理单个子节点
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
      }
      //处理多子节点
      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild,
          lanes,
        );
      }
    }
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }

单节点

  function reconcileSingleElement(
    returnFiber: Fiber,   
    currentFirstChild: Fiber | null, 
    element: ReactElement,
    lanes: Lanes,
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      if (child.key === key) {
        const elementType = element.type;
          if (child.elementType === elementType) {
            deleteRemainingChildren(returnFiber, child.sibling);
            const existing = useFiber(child, element.props);
            existing.return = returnFiber;
            return existing;
          }
        deleteRemainingChildren(returnFiber, child);
        break;
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }
     const created = createFiberFromElement(element, returnFiber.mode, lanes);
     created.return = returnFiber;
     return created;
  }

从上面代码中可以看到,这里有一个常见的误区,react的diff算法比较的是fiber和reactElement,而不是fiber和fiber进行对比。可以看到这个方法主要由一个大的while循环构成,接下来详细解释下循环中做的事

进入while循环中,根据key和type的不同,又分为如下几种情况:

1.key相同,type不同

例如如下情况

这种情况说明元素的节点类型发生了变化,p---->div,直接调用deleteRemainingChildren(returnFiber, child) ,删除旧的p fiber及它后面的所有fiber (删除旧的p fiber 和div fiber),跳出循环,执行创建fiber的代码,此时就不会进入复用逻辑,

const created = createFiberFromElement(element, returnFiber.mode, lanes);
created.return = returnFiber;

2. key不同

执行deleteChild(returnFiber, child) ,此处就是删除key为1的div fiber , 然后执行child = child.sibling,此时child变成key为3的div fiber, 再次执行循环体

3.key相同,type相同

当key和type都相同,即会进入fiber复用逻辑,可以看到里面调用了useFiber方法,该方法会根据传入的props尽量复用旧的fiber

deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, element.props);
existing.return = returnFiber;

多节点

当新的children为多节点,会进入多节点reconcile流程,我们以下图为例

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array,
    lanes: Lanes,
  ): Fiber | null {
    let resultingFirstChild: Fiber | null = null;
    let previousNewFiber: Fiber | null = null;
    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;
    let nextOldFiber = null;
    //第一次循环,遍历前后最大长度,复用oldFiber,直到key不一样就跳出
    for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      } else {
        nextOldFiber = oldFiber.sibling;
      }
      //updateSlot方法内部就是去判断key是否相同,相同且type也相同的话就复用,key不同的话就返回null
      const newFiber = updateSlot(
        returnFiber,
        oldFiber,
        newChildren[newIdx],
        lanes,
      );
      if (newFiber === null) {
        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) {
        resultingFirstChild = newFiber;
      } else {
        previousNewFiber.sibling = newFiber;
      }
      previousNewFiber = newFiber;
      oldFiber = nextOldFiber;
    }
​
    //新数组被遍历完了,old Fiber没遍历的就可以删了
    if (newIdx === newChildren.length) {
      // We've reached the end of the new children. We can delete the rest.
      deleteRemainingChildren(returnFiber, oldFiber);
      return resultingFirstChild;
    }
​
    //旧的被遍历完了,但是新的还有,新的剩下的打上Placement标记
    if (oldFiber === null) {
      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);
​
    // 第二次循环: 遍历剩余非公共序列, 优先复用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) {
    //如果map中的fiber被复用了,则说明我们不需要给他打上deletion 标记,需要将它从map中删除(最终map中的fiber都是需要被删除的)
            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;
  }

上面的源码中,我们对其中的关键步骤进行讲解。

首先会进入一次循环

本次循环最多会循环旧数组和新数组的最小长度次,以图中的例子来看就是5次

每次循环都会执行updateSlot方法,方法内部会判断新旧节点的key是否相同,这里会出现几种情况:

可以看出,第一次大的循环会一直往后遍历,尽最大可能的复用旧的节点,直到新旧节点的key不同,就会跳出循环,跳出第一次循环会产生3种不同的可能性

新数组被遍历完,旧的还有剩下的

这种情况只需要删除旧数组中剩下的即可,结束diff算法流程

旧数组被遍历完,新数组还有剩下的

这种情况说明旧数组都得到了复用,只需要给新数组中剩下的打上新增标记即可,结束diff算法流程

新旧数组都还有剩下的

这种情况是最复杂的,因此会进入diff算法后续流程

第一轮循环结束,新旧数组都还有剩余

首先会把旧数组中剩余的节点放入一个map中,以节点的key为键,方便后续通过key进行查找

接下来就会对剩余的新数组进行遍历,遍历过程中拿到每一个节点的key,去map里面找是否有能匹配上的节点,如果key和type都匹配上了则复用旧的节点,当节点得到了复用,需要从map中移除对应的旧节点,因为运行到最后map里面保留的就是所有没有得到复用的节点,需要打上删除标记

当一个节点得到了复用,这个节点所在的位置可能发生了变化,例如

A->B->C->D->E 变成 A->B->E->C->X->Y,其中C和E的相对位置发生了变化,react内部是如何确定节点的位置变化了呢?确定节点是否发生移动的方法就是placeChild

这里涉及到了一个关键的变量,lastPlacedIndex,这个变量的含义可以理解为目前已经复用的节点在旧数组中最大的index,读起来很拗口,通过上面的例子来解释:当遍历到了A和B节点时,发现可以被复用,则lastPlacedIndex被修改为B节点在旧数组中的index,也就是1,再往后遍历到了E,此时E的旧index为4,大于lastPlacedIndex,则此时E节点是相对旧数组没有发生移动的,因此不会给E节点标记为移动,且会把lastPlacedIndex设置为4。继续遍历到C,C的旧index为2,lastPlacedIndex为4,因此可以知道在旧数组中,当前遍历的节点在上一个节点之前,因此当前节点是需要移动的。

由此可以看出react对节点移动的策略是:让元素往后移动,避免元素往前移动

 function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  ): number {
    newFiber.index = newIndex;
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        //应该是尽量让节点往后移动,避免节点往前移动
        // This is a move.
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        // This item can stay in place.
        return oldIndex;
      }
    } else {
      // This is an insertion.
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
  }