React源码解析之FunctionComponent(下)

1,047 阅读22分钟

前言:
React源码解析之FunctionComponent(中) 中,讲到了reconcileSingleElement()reconcileSingleTextNode()

function reconcileChildFibers({
  if (isObject) {
      switch (newChild.?typeof) {
        // ReactElement节点
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement());
      }
    }
    //文本节点
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode());
    }
    //数组节点,也是本文要讲的
    if (isArray(newChild)) {
      return reconcileChildrenArray();
    }
}

接下来,我们讲reconcileChildrenArray()是如何更新数组节点的

一、reconcileChildrenArray
作用:
更新数组节点

源码:

  function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    //待更新的数组节点
    newChildren: Array<*>,
    expirationTime: ExpirationTime,
  
): 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.

    //删除了 dev 代码

    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++) {
      // 当要更新的节点的 index 大于 newIndex 时,
      // 说明它不在所期盼的位置上,则需要“认真处理”oldFiber
      if (oldFiber.index > newIdx) {
        nextOldFiber = oldFiber;
        oldFiber = null;
      }
      //否则,则处理该节点的下一个兄弟节点
      else {
        nextOldFiber = oldFiber.sibling;
      }
      //复用或新建节点
      const newFiber = updateSlot(
        //当前节点的父节点
        returnFiber,
        //旧节点
        oldFiber,
        //待更新的新节点
        newChildren[newIdx],
        expirationTime,
      );
      //说明key 不相同,节点不能复用,此时就跳出循环
      //如果不跳出循环,说明可以是相同的
      //也就是说当跳出循环的时候,我们可以知道截至目前,复用节点的个数,和不可复用节点的 index,
      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) {
        //newFiber.alternate表示并没有复用 oldFiber 来赋值,而是 return 了新的 fiber
        //所以要删除存在的 旧的fiber
        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);
        }
      }
      //将 newFiber 节点挂载到 DOM 树上,返回最新移动的 index
      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;
    }
    //跳出循环后
    //index=length,说明截止到最后,所有节点都是可以复用的
    //故可以删除老节点
    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],
          expirationTime,
        );
        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.
    //数组可能存在顺序的变化,oldfiber和 newfiber 还有可以复用的 fiber 节点
    const existingChildren = mapRemainingChildren(returnFiber, oldFiber);

    // Keep scanning and use the map to restore deleted items as moves.
    //继续遍历剩下的 new 节点
    for (; newIdx < newChildren.length; newIdx++) {
      const newFiber = updateFromMap(
        existingChildren,
        returnFiber,
        newIdx,
        newChildren[newIdx],
        expirationTime,
      );
      if (newFiber !== null) {
        if (shouldTrackSideEffects) {
          //不为 null 说明 fiber 节点已经被复用了,所以可以从 Map 中删除
          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;
  }

解析:
(1) 循环数组节点,在循环中主要做了如下几点:

① 将 oldFiber 的 index与 newIdx 进行比较,
如果 oldFIber.index 大,则将 oldFiber 赋值给 nextOldFiber(表示需要处理);
如果 newIdx 大,则将 oldFiber.sibling 赋值给 nextOldFiber

② 执行updateSlot(),复用或新建节点,返回的结果赋值给newFiber

③ 如果newFiber的值为空的话,说明该节点不能复用,则跳出循环(break

④ 如果是第一次渲染(即shouldTrackSideEffects为 true),并且 newFiber 没有要复用的 oldFiber 的话,则删除该 fiber 下的所有子节点

关于deleteChild的讲解,请看:React源码解析之FunctionComponent(中)

⑤ 执行placeChild(),将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index,并赋值给lastPlacedIndex

previousNewFiber那段,意思是为数组里的每一个 fiber 节点设置 sibling 属性,即它旁边的 fiber(index+1)

(2) 跳出循环后,如果newIdx和更新的数组长度相等,则表示所有节点都是可以复用的,那么就执行deleteRemainingChildren(),删除旧节点

(3) 如果旧节点都已经被复用完了,但是仍有部分新节点需要被创建的话,则循环剩余数组的长度,并依次创建新节点(部分代码与上面重复,不再赘述)

(4) 如果仍有旧节点剩余的话,则执行mapRemainingChildren(),将这些旧节点用 Map 结构集合起来,看有没有方便 newFiber 复用的节点

(5) 继续遍历剩下的 new 节点
① 执行updateFromMap(),查找有没有 key/index 相同的点,方便复用

if (newFiber !== null)的部分逻辑与上面相同,不再赘述

(6) 如果是第一次渲染的话,则删除没有复用的节点

(7) 最终返回 更新后的数组的第一个节点(根据它的 silbing 属性,可找到其他节点)

后面的部分是针对reconcileChildrenArray()出现的一些函数的补充

二、updateSlot
作用:
复用或新建节点

源码:

  //复用或新建节点

  //key 相同的情况下,进行节点复用;
  //key 不同的情况下,无法复用
  function updateSlot(
    //当前节点的父节点
    returnFiber: Fiber,
    //旧节点
    oldFiber: Fiber | null,
    //待更新的新节点
    newChild: any,
    expirationTime: ExpirationTime,
  
): Fiber | null 
{
    // Update the fiber if the keys match, otherwise return null.

    const key = oldFiber !== null ? oldFiber.key : null;
    //文本节点是没有 key 的
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // Text nodes don't have keys. If the previous node is implicitly keyed
      // we can continue to replace it without aborting even if it is not a text
      // node.
      //如果老节点有 key 的话,说明是从 ReactElement 节点转变为文本节点了
      // 这样也没关系,可以不间断更新
      if (key !== null) {
        return null;
      }
      //执行updateTextNode,对文本节点进行更新
      return updateTextNode(
        returnFiber,
        oldFiber,
        '' + newChild,
        expirationTime,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.?typeof) {
        case REACT_ELEMENT_TYPE: {
          //前后 key 相同,说明可以复用
          if (newChild.key === key) {
            if (newChild.type === REACT_FRAGMENT_TYPE) {
              return updateFragment(
                returnFiber,
                oldFiber,
                newChild.props.children,
                expirationTime,
                key,
              );
            }
            return updateElement(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          }
          //否则不能复用
          else {
            return null;
          }
        }
        case REACT_PORTAL_TYPE: {
          if (newChild.key === key) {
            return updatePortal(
              returnFiber,
              oldFiber,
              newChild,
              expirationTime,
            );
          } else {
            return null;
          }
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        if (key !== null) {
          return null;
        }

        return updateFragment(
          returnFiber,
          oldFiber,
          newChild,
          expirationTime,
          null,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType();
      }
    }

    return null;
  }

解析:
(1) 如果是文本节点的话,是不能根据 key 去判断是否复用的,注意下

(2) 如果是其他节点类型的话,则执行相应的函数,来进行节点更新(key 相同则复用)

三、placeChild
作用:
将 newFiber 节点挂载到 DOM 树上,并判断更新后是否移动过,如果移动,则需要重新挂载,返回最新移动的 index

源码:

  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number,
  
): number 
{
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      // Noop.
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      //移动了的节点
      if (oldIndex < lastPlacedIndex) {
        // This is a move.
        //因为是移动的节点,所以要重新挂载到 DOM 上
        newFiber.effectTag = Placement;
        return lastPlacedIndex;
      } else {
        //没有移动
        // This item can stay in place.
        return oldIndex;
      }
    }
    //current 为 null 说明该节点没有被渲染过
    //所以是新插入的节点
    else {
      // This is an insertion.
      newFiber.effectTag = Placement;
      return lastPlacedIndex;
    }
  }

解析:
(1) 如果不是初次渲染的话(shouldTrackSideEffects 为 true),无需更新shouldTrackSideEffects

(2) newFiber.alternate有值的话,说明是由旧节点更新来的,那么就需要比较oldIndexlastPlacedIndex,有移动过的话,则返回lastPlacedIndex,否则返回oldIndex

(3) newFiber.alternate没有值的话,说明不是由旧节点更新来的,而是新插入的节点,返回lastPlacedIndex

四、mapRemainingChildren
作用:
将旧节点用 Map 结构集合起来,方便 newFiber 复用

源码:

  function mapRemainingChildren(
    returnFiber: Fiber,
    currentFirstChild: Fiber,
  
): Map<string | number, Fiber> 
{
    // Add the remaining children to a temporary map so that we can find them by
    // keys quickly. Implicit (null) keys get added to this set with their index
    // instead.
    const existingChildren: Map<string | number, Fiber> = new Map();

    let existingChild = currentFirstChild;
    //遍历剩下的节点,获取其 key
    while (existingChild !== null) {
      if (existingChild.key !== null) {
        existingChildren.set(existingChild.key, existingChild);
      } else {
        existingChildren.set(existingChild.index, existingChild);
      }
      existingChild = existingChild.sibling;
    }
    //创建了一个 Map 对象,以便找到key 相同的节点,方便复用
    return existingChildren;
  }

解析:
利用 Map 结构,遍历剩下的 oldFiber,以key-value的形式,将这些旧节点存到 Map 中,如果没有key的话,则说明是文本节点,则以index-value的形式存储,最终返回这个 Map 对象

五、updateFromMap
作用:
在 Map 对象中查找有没有 key/index 相同的 fiber 节点,方便复用

源码:

function updateFromMap(
    existingChildren: Map<string | number, Fiber>,
    returnFiber: Fiber,
    newIdx: number,
    newChild: any,
    expirationTime: ExpirationTime,
  
): Fiber | null 
{
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      // Text nodes don't have keys, so we neither have to check the old nor
      // new node for the key. If both are text nodes, they match.
      //如果是文本节点的话,会从 Map 对象中寻找是否有相同的 index(为什么不是key?因为文本节点没有 key 属性)
      const matchedFiber = existingChildren.get(newIdx) || null;
      return updateTextNode(
        returnFiber,
        matchedFiber,
        '' + newChild,
        expirationTime,
      );
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.?typeof) {
        case REACT_ELEMENT_TYPE: {
          //updateSlot()是根据 key 是否相同来判断,这边是根据 Map 中是否有key/index 来判断
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null;
          if (newChild.type === REACT_FRAGMENT_TYPE) {
            return updateFragment(
              returnFiber,
              matchedFiber,
              newChild.props.children,
              expirationTime,
              newChild.key,
            );
          }
          return updateElement(
            returnFiber,
            matchedFiber,
            newChild,
            expirationTime,
          );
        }
        case REACT_PORTAL_TYPE: {
          const matchedFiber =
            existingChildren.get(
              newChild.key === null ? newIdx : newChild.key,
            ) || null;
          return updatePortal(
            returnFiber,
            matchedFiber,
            newChild,
            expirationTime,
          );
        }
      }

      if (isArray(newChild) || getIteratorFn(newChild)) {
        const matchedFiber = existingChildren.get(newIdx) || null;
        return updateFragment(
          returnFiber,
          matchedFiber,
          newChild,
          expirationTime,
          null,
        );
      }

      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType();
      }
    }

    return null;
  }

解析:
二、updateSlot的内容差不多,不再赘述

关于FunctionComponent的更新讲解就先到这里结束了

GitHub:
ReactChildFiber


(完)