React 源码系列:reconcileChildrenArray

561 阅读3分钟

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

今天来填昨天的坑,来好好研究一下 reconcileChildrenArray,它有什么锤子用,怎么实现的?

函数作用

它的函数签名如下:

  function reconcileChildrenArray(
 
   returnFiber: Fiber,
 
   currentFirstChild: Fiber | null,
 
   newChildren: Array<*>,
 
   lanes: Lanes,
 
  ): Fiber | null {}

函数是用于渲染组件的子元素列表的。returnFiber 需要和 newChildren 做 diff,然后返回重建的 Fiber。

需要用到的其他函数

  • updateSlot:更新 fiber,最终都是调用 createFiber 创建一个新的 fiber 以表示更新后的结果fiber 并返回
  • deleteRemainingChildren:删除剩余的孩子 fiber
  • mapRemainingChildren: 返回一个 map,给剩余的孩子 fiber 建立一个 key map,即键为 fiber的 key 属性(不存在则使用 index 充当 key),值为 fiber
  • createChild:根据 newChild 的值创建新的 Fiber
  • placeChild:更新 newFiber 在新的后代 fiber 列表中的位置 index,同时根据 index 与 lastPlacedIndex 的前后关系决定是否标记 newFiber.flags 是否加上 Placement
  • deleteChild:将被删的孩子 fiber 加入到 returnFibers.deletions 数组中,returnFibers.flags 加上 ChildDeletion,以便执行 useEffect、useLayoutEffect 等副作用钩子的清除函数
  • getIsHydrating:检查 isHydrating 是否为 true
  • pushTreeFork:将孩子fiber 列表的父 fiber 和孩子 fiber 数入栈

代码拆解

代码可以拆成下面几个部分:

  1. 变量准备
  2. 第一个 for 循环
  3. 两个 for 循环代码块之间的操作
  4. 第二个 for 循环
  5. 第二个 for 循环之后的操作

变量准备

     let resultingFirstChild: Fiber | null = null;
     let previousNewFiber: Fiber | null = null;
 ​
     let oldFiber = currentFirstChild;
     let lastPlacedIndex = 0;
     let newIdx = 0;
     let nextOldFiber = null;

resultingFirstChild 用作返回值,表示与 newChildren 同步后的后代 fiber 的第一个 fiber。

previousNewFiber 表示上一个新建的 Fiber,用于将当前新建的 Fiber 链到 previousNewFiber 的 sibling 域,以长子-兄弟链表表示法构建基于 newChildren、以 returnFiber 为根的新 Fiber 树。

oldFiber 表示正在处理的旧 Fiber。

lastPlacedIndex 表示当前已经新建的 Fiber 的 index 的最大值,为 placeChild 函数服务。

newIdx 表示遍历 newChildren 的索引指针

nextOldFiber 表示 oldFiber 的下一个右紧邻兄弟 fiber。

第一个 for 循环

     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) {
         if (oldFiber === null) {
           oldFiber = nextOldFiber;
         }
         break;
       }
       if (shouldTrackSideEffects) {
         if (oldFiber && newFiber.alternate === null) {
           deleteChild(returnFiber, oldFiber);
         }
       }
       lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
       if (previousNewFiber === null) {
         resultingFirstChild = newFiber;
       } else {
         previousNewFiber.sibling = newFiber;
       }
       previousNewFiber = newFiber;
       oldFiber = nextOldFiber;
     }

遍历 newChildren,对于 oldFiber.index 小于等于 newIdx 的,执行 updateSlot 得到新 fiber,如果需要跟踪副作用,判断新旧 fiber 的 diff结果是否为删除,如果是则通过 deleteChild 将被删 fiber 加到 returnFiber 的 deletions 中;调用 placeChild 确定 newFiber 的 index 并条件性添加 Placement flag。循环中新建的 fiber 会通过 sibling 域连成一个 sibling 链表。

两个 for 循环之间的代码

如果跳出循环的时候,newChildren的元素已经被遍历完了,则将旧 fiber 树剩余的孩子 fiber “删除”(对剩余的旧 fiber 全部调 deleteChild`),返回新 fiber 树的长子,即 resultingFirstChild。

反之,如果跳出第一个 for 循环的时候,newChildren 还有元素没有遍历完,这些元素都是要待插入的新 fiber,执行第二个 for 循环。

第二个 for 循环

       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;
       }

说白了,就是为多出来的 newChildren 建立一个个 newFiber(通过 createChild 创建),依次尾插到前面建立的新 fiber 树的 sibling 链表中,最终reconcileChildrenArray返回的是 newChildren 对应的新 fiber 树的“长子”,即 resultingFirstChild。