reconcileChildren的解读

2022-08-19 15:16:49 浏览数 (1)

reconcileChildren

dom diff的入口函数就是reconcileChildren,那么他的源码如下:

代码语言:javascript复制
//packages/react-reconciler/src/ReactFiberBeginWork.old.js
export function reconcileChildren(
  current: Fiber | null,//当前的fiber节点
  workInProgress: Fiber,// 新生成的fiber
  nextChildren: any,//  新生成的reactElement内容
  renderLanes: Lanes,//渲染优先级
) {
  if (current === null) {
    // 如果没有已经渲染的fiber树,则直接把reactElement内容渲染上去
    // If this is a fresh new component that hasn't been rendered yet, we
    // won't update its child set by applying minimal side-effects. Instead,
    // we will add them all to the child before it gets rendered. That means
    // we can optimize this reconciliation pass by not tracking side-effects.
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // If the current child is the same as the work in progress, it means that
    // we haven't yet started any work on these children. Therefore, we use
    // the clone algorithm to create a copy of all the current children.

    // If we had any progressed work already, that is invalid at this point so
    // let's throw it out.
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}
复制代码

reconcileChildren的源码并不长,主要做了两件事

  • 如果是首次渲染,则会把已经处理好的fiber树进行挂载。
  • 如果不是首次渲染则调用reconcileChildFibers进行下一步处理。

我们关注一下mountChildFibersreconcileChildFibers,我们发现这两个函数分别指向ChildReconciler,只是mountChildFibers的参数为falsereconcileChildFibers的参数为true。我们在这里先埋下一个点,看看这个参数对后期的流程有什么影响。

我们继续深入可以发现,ChildReconciler这个函数冰的执行返回了reconcileChildFibers,所以这便是reconcileChildren的核心功能代码所在了。

reconcileChildFibers

代码语言:javascript复制
 function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any,
    lanes: Lanes,
  ): Fiber | null {
    // This function is not recursive.
    // If the top level item is an array, we treat it as a set of children,
    // not as a fragment. Nested arrays on the other hand will be treated as
    // fragment nodes. Recursion happens at the normal flow.

    // Handle top level unkeyed fragments as if they were arrays.
    // This leads to an ambiguity between <>{[...]}</> and <>...</>.
    // We treat the ambiguous cases above the same.
    const isUnkeyedTopLevelFragment =
      typeof newChild === 'object' &&
      newChild !== null &&
      newChild.type === REACT_FRAGMENT_TYPE &&
      newChild.key === null;
    if (isUnkeyedTopLevelFragment) {
      newChild = newChild.props.children;
    }

    // Handle object types
    const isObject = typeof newChild === 'object' && newChild !== null;

    // 处理对象类型
    if (isObject) {
      switch (newChild.$$typeof) {
        // REACT_ELEMENT_TYPE类型
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        // REACT_PORTAL_TYPE类型  
        case REACT_PORTAL_TYPE:
          return placeSingleChild(
            reconcileSinglePortal(
              returnFiber,
              currentFirstChild,
              newChild,
              lanes,
            ),
          );
        // REACT_LAZY_TYPE类型    
        case REACT_LAZY_TYPE:
          if (enableLazyElements) {
            const payload = newChild._payload;
            const init = newChild._init;
            // TODO: This function is supposed to be non-recursive.
            return reconcileChildFibers(
              returnFiber,
              currentFirstChild,
              init(payload),
              lanes,
            );
          }
      }
    }

    // 字符串与数字类型
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          ''   newChild,
          lanes,
        ),
      );
    }
    // 数组类型 
    if (isArray(newChild)) {
      return reconcileChildrenArray(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }
    
    // 可迭代的类型
    if (getIteratorFn(newChild)) {
      return reconcileChildrenIterator(
        returnFiber,
        currentFirstChild,
        newChild,
        lanes,
      );
    }

    if (isObject) {
      throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (__DEV__) {
      if (typeof newChild === 'function') {
        warnOnFunctionType(returnFiber);
      }
    }
    if (typeof newChild === 'undefined' && !isUnkeyedTopLevelFragment) {
      // If the new child is undefined, and the return fiber is a composite
      // component, throw an error. If Fiber return types are disabled,
      // we already threw above.
      switch (returnFiber.tag) {
        case ClassComponent: {
          if (__DEV__) {
            const instance = returnFiber.stateNode;
            if (instance.render._isMockFunction) {
              // We allow auto-mocks to proceed as if they're returning null.
              break;
            }
          }
        }
        // Intentionally fall through to the next case, which handles both
        // functions and classes
        // eslint-disable-next-lined no-fallthrough
        case Block:
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent: {
          invariant(
            false,
            '%s(...): Nothing was returned from render. This usually means a '  
              'return statement is missing. Or, to render nothing, '  
              'return null.',
            getComponentName(returnFiber.type) || 'Component',
          );
        }
      }
    }

    // Remaining cases are all treated as empty.
    return deleteRemainingChildren(returnFiber, currentFirstChild);
  }
复制代码

reconcileChildFibers中我们根据入参newChild的类型分别对应着不同的处理:

  • 当修改的内容为REACT_ELEMENT_TYPE类型,调用reconcileSingleElement函数。
  • 当修改的内容为REACT_PORTAL_TYPE类型,调用reconcileSinglePortal函数。
  • 当修改的内容为REACT_LAZY_TYPE类型,递归调用reconcileChildFibers函数。
  • 当修改的内容问纯文本类型,调用reconcileSingleTextNode函数。
  • 当修改的内容为数组类型,调用reconcileChildrenArray函数。
  • 当修改的内容为可迭代类型,调用reconcileChildrenIterator函数

reconcileSingleElement

reconcileSingleElement的源码如下:

代码语言:javascript复制
  function reconcileSingleElement(
    returnFiber: Fiber,// 父级
    currentFirstChild: Fiber | null, // 父级下diff的第一个
    element: ReactElement, // 当前元素
    lanes: Lanes, // 优先级
  ): Fiber {
    const key = element.key;
    let child = currentFirstChild;
    while (child !== null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      if (child.key === key) {
        switch (child.tag) {
          // 如果为Fragment类型,并且key也相等
          case Fragment: {
            if (element.type === REACT_FRAGMENT_TYPE) {

              // get后面的兄弟节点添加Deletion标记,用于dom删除
              deleteRemainingChildren(returnFiber, child.sibling);

              // 通过useFiber复用旧fiber与新的props
              const existing = useFiber(child, element.props.children);
              existing.return = returnFiber;
              if (__DEV__) {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            }
            break;
          }
          case Block:
            if (enableBlocksAPI) {
              let type = element.type;
              if (type.$$typeof === REACT_LAZY_TYPE) {
                type = resolveLazyType(type);
              }
              if (type.$$typeof === REACT_BLOCK_TYPE) {
                // The new Block might not be initialized yet. We need to initialize
                // it in case initializing it turns out it would match.
                if (
                  ((type: any): BlockComponent<any, any>)._render ===
                  (child.type: BlockComponent<any, any>)._render
                ) {
                  deleteRemainingChildren(returnFiber, child.sibling);
                  const existing = useFiber(child, element.props);
                  existing.type = type;
                  existing.return = returnFiber;
                  if (__DEV__) {
                    existing._debugSource = element._source;
                    existing._debugOwner = element._owner;
                  }
                  return existing;
                }
              }
            }
          // We intentionally fallthrough here if enableBlocksAPI is not on.
          // eslint-disable-next-lined no-fallthrough
          default: {
            if (
              // 新的ReactElement与旧的current fiber 的key 与 type都相同
              child.elementType === element.type ||
              // Keep this check inline so it only runs on the false path:
              (__DEV__
                ? isCompatibleFamilyForHotReloading(child, element)
                : false)
            ) {
               // 添加标记
              deleteRemainingChildren(returnFiber, child.sibling);
              const existing = useFiber(child, element.props);
              existing.ref = coerceRef(returnFiber, child, element);
              existing.return = returnFiber; 
              if (__DEV__) {
                existing._debugSource = element._source;
                existing._debugOwner = element._owner;
              }
              return existing;
            }
            break;
          }
        }
        // 匹配不上,key相等,type不相等,移除旧的fiber以及后面的兄弟
        deleteRemainingChildren(returnFiber, child);
        break;
      } 


      else 
      
      
      
      {
        // 如果key不同,则标记Deletion,
        deleteChild(returnFiber, child);
      }
      // 遍历其兄弟
      child = child.sibling;
    }

    if (element.type === REACT_FRAGMENT_TYPE) {
      // 如果是fragment类型,创建fragment,并返回。
      const created = createFiberFromFragment(
        element.props.children,
        returnFiber.mode,
        lanes,
        element.key,
      );
      created.return = returnFiber;
      return created;
    } else {
      //如果不是fragment,创建element并返回fiber
      const created = createFiberFromElement(element, returnFiber.mode, lanes);
      created.ref = coerceRef(returnFiber, currentFirstChild, element);
      created.return = returnFiber;
      return created;
    }
  }
复制代码

根据源码,reconcileSingleElement函数中会遍历当前父级fiber下面的所有子fiber,根据旧的fiber与新生成的ReactElement的keytype进行比较:

  • 如果旧的fiber子节点与新的子节点的keytype不一致,给当前的旧的fiber子节点添加上Deletion标记,继续遍历其兄弟节点。
  • 如果旧的fiber子节点与新的子节点的key是一致的,就会根据当前的节点类型去做匹配处理,通过deleteRemainingChildren给当前子节点以及后面的所有的兄弟节点添加上Deletion标记,并且通过useFiber复用该子节点和该子节点新的props
  • 如果旧的fiber子节点与新的子节点的类型匹配不上,则会直接给旧的fiber子节点打上Deletion标记,移除子节点以及后面的所有兄弟节点。
  • 如果旧的fiber树遍历完毕,但是发现还没有匹配完的节点,那么会通过createFiberFromFragmentcreateFiberFromElement创建新的fiber节点,并指向父级fiber

reconcileSingPortal

代码语言:javascript复制
 function reconcileSinglePortal(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    portal: ReactPortal,
    lanes: Lanes,
  ): Fiber {
    const key = portal.key;
    let child = currentFirstChild;
    while (child !== null) {
      // TODO: If key === null and child.key === null, then this only applies to
      // the first item in the list.
      if (child.key === key) {
        if (
          child.tag === HostPortal &&
          child.stateNode.containerInfo === portal.containerInfo &&
          child.stateNode.implementation === portal.implementation
        ) {
          deleteRemainingChildren(returnFiber, child.sibling);
          const existing = useFiber(child, portal.children || []);
          existing.return = returnFiber;
          return existing;
        } else {
          deleteRemainingChildren(returnFiber, child);
          break;
        }
      } else {
        deleteChild(returnFiber, child);
      }
      child = child.sibling;
    }

    const created = createFiberFromPortal(portal, returnFiber.mode, lanes);
    created.return = returnFiber;
    return created;
  }
复制代码

有了上面REACT_ELEMENT_TYPE的讲解,对于REACT_PORTAL_TYPE的源码就有一定的思路了,如果还不知道ReactPortal的作用,直通车 >> Protal有何用处

placeSingleChild

上述的不管是REACT_ELEMENT_TYPEREACT_PORTAL_TYPEREACT_LAZY_TYPE都是用了placeSingleChild包裹起来的,我们来看一看他做了什么事情。

代码语言:javascript复制
  function placeSingleChild(newFiber: Fiber): Fiber {
    // This is simpler for the single child case. We only need to do a
    // placement for inserting new children.
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags = Placement;
    }
    return newFiber;
  }
复制代码

那么这里我们就发现了这个shouldTrackSideEffects,还记得我们在前面讲的ChildReconciler函数的入参吗?他只是一个布尔。在挂载阶段shouldTrackSideEffects:false,直接是return newFiber。不必要的标记增加性能开销。而在更新阶段,就必须要做这样的操作。Placement为dom更新时的插入标记。

0 人点赞