React 源码系列:reconcileSingleElement 是啥

857 阅读3分钟

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

上一篇文章提到了 reconcileSingleElement 调用,今天来看看这个代码做了啥。

源码分析

函数签名如下:

   function reconcileSingleElement(
     returnFiber: Fiber,
     currentFirstChild: Fiber | null,
     element: ReactElement,
     lanes: Lanes,
   ): Fiber {}

事实上,currentFirstChild 是 returnFiber.alternate.child。我们以前提过,Fiber 除了有 child 字段,还有 sibling 字段,套用曾经学过的数据结构课程的术语就是使用长子-兄弟链表表示法表示一棵树的结构。

image.png

核心实现(省略了 lazyElement 相关代码)见下方代码,可以看到其中使用了很多函数。

  • deleteRemainingChildren(returnFiber, currentFirstChild):从 returnFiber 中删除 currentFirstChild 自身和它的右边的所有兄弟 fiber
  • useFiber(fiber, pendingProps):以 child.alternate 为蓝本,创建一个副本 fiber
  • coerceRef(returnFiber, current, element): 解析 element 中的 ref 属性,不合法则 throw 各种类型的 error
  • deleteChild(returnFiber, childToDelete):从 returnFiber 中删除 childToDelete
  • createFiberFromFragment:根据 Fragment 元素创建一个 fiber
   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 (elementType === REACT_FRAGMENT_TYPE) {
           if (child.tag === Fragment) {
             deleteRemainingChildren(returnFiber, child.sibling);
             const existing = useFiber(child, element.props.children);
             existing.return = returnFiber;
             return existing;
           }
         } else {
           if (
             child.elementType === elementType
           ) {
             deleteRemainingChildren(returnFiber, child.sibling);
             const existing = useFiber(child, element.props);
             existing.ref = coerceRef(returnFiber, child, element);
             existing.return = returnFiber;
             return existing;
           }
         }
         // 没有匹配上
         deleteRemainingChildren(returnFiber, child);
         break;
       } else {
         deleteChild(returnFiber, child);
       }
       child = child.sibling;
     }
 ​
     if (element.type === REACT_FRAGMENT_TYPE) {
       const created = createFiberFromFragment(
         element.props.children,
         returnFiber.mode,
         lanes,
         element.key,
       );
       created.return = returnFiber;
       return created;
     } else {
       const created = createFiberFromElement(element, returnFiber.mode, lanes);
       created.ref = coerceRef(returnFiber, currentFirstChild, element);
       created.return = returnFiber;
       return created;
     }
   }

reconcileSingleElement 的功能简单来说就是下面所述。

  • 通过 child 和 sibling 遍历 returnFiber 的所有孩子 Fiber,检查 key 是否命中 element.key
  • 删除 returnFiber 中没有命中 key 的 child,只保留命中的 child,通过 useFiber 创建一个新的 Fiber(如果 element 是一个 Fragment,则以 element.props.children 建立 Fiber),将returnFiber 赋给新的 fiber 的 return 字段,然后返回这个新的fiber。
  • 如果 returnFiber 没有一个命中 key,则通过 createFiberFromElement或者createFiberFromFragment创建一个新的 fiber,这个fiber的key 就是 element.key,将 returnFiber 作为 新建 Fiber 的 return 字段,最后返回新建 Fiber

记得在被调用的时候 reconcileSingleElement 的返回值被 `placeSingleChild 包裹了,这个作用只是给返回的 fiber 更新一下 flags 字段。

   function placeSingleChild(newFiber: Fiber): Fiber {
     // This is simpler for the single child case. We only need to do a
     // placement for inserting new children.
     // Placement 标识表示当我们需要在原来的 dom 上插入新的 children 只需要做一个替换
     if (shouldTrackSideEffects && newFiber.alternate === null) {
       newFiber.flags |= Placement;
     }
     return newFiber;
   }

reconcileSingleElement 讲完了,那就回到上一层的 reconcileChildFibers函数看看,发现如果 newChild 是一个数组,则会走reconcileChildrenArray逻辑

reconcileChildrenArray

这个函数是关于 newChildren 是数组时如何创建或更新 Fiber 的,比较复杂,下一篇文章会展开详细介绍,这里先讲一点点容易看懂的。

在其中有一个重要的函数调用叫 updateSlot,作用是根据 newChild 的内容更新对应的Fiber,逻辑如下:

如果 newChild 是一个number 或 string 类型的值,说明这是一个文本节点,则调 updateTextNode并返回

否则如果 newChild 是一个非空对象,则根据 newChild.$$typeof的值给 key 与 newChild.key 相同的 oldFiber 调 updateElementupdatePortalupdateSlot

如果 newChild 是一个数组或可迭代的对象,则检查 oldFiber 有没有 key,如果没有则返回 null,否则调 updateFragment