React 源码学习之 commit 阶段

1,110 阅读11分钟

nubelson-fernandes-Bv2FCJoh8XM-unsplash.jpg

图片来源 unsplash.com/photos/Bv2F…

前言

commitRoot 方法是 commit 阶段工作的起点。

function commitRoot(root) {
  // TODO: This no longer makes any sense. We already wrap the mutation and
  // layout phases. Should be able to remove.
  const previousUpdateLanePriority = getCurrentUpdatePriority();
  const prevTransition = ReactCurrentBatchConfig.transition;
  try {
    ReactCurrentBatchConfig.transition = 0;
    setCurrentUpdatePriority(DiscreteEventPriority);
    commitRootImpl(root, previousUpdateLanePriority);
  } finally {
    ReactCurrentBatchConfig.transition = prevTransition;
    setCurrentUpdatePriority(previousUpdateLanePriority);
  }

  return null;
}

commitRoot 方法源码:github.com/facebook/re…

WeChat4b7f87841c4f1efbb8128b9fa8bde6a3.png

commitRoot(root)方法中的 root 参数 FiberRootNode

WeChatbe4ecf035535490bf851a8f076bc581e.png

调度 useEffect

useEffect 的函数会在浏览器完成布局与绘制之后,在 scheduleCallback 一个延迟事件中被调用。在 commitRootImpl 方法中会异步调度回调函数 flushPassiveEffects 方法,它会触发useEffect 方法。

可以在 这里 看到源码

 if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      scheduleCallback(NormalSchedulerPriority, () => {
        // 触发useEffect
        flushPassiveEffects();
        return null;
      });
    }
  }

commit 阶段的主要内容

commit 阶段的主要工作(即 Renderer 的工作流程)分为三部分:

  1. before mutation 阶段(执行 DOM 操作前)

  2. mutation 阶段(执行 DOM 操作)

  3. layout 阶段(执行 DOM 操作后)

before mutation 阶段

可以在 这里 看到部分源码

 const prevTransition = ReactCurrentBatchConfig.transition;
    ReactCurrentBatchConfig.transition = 0;
    // 保存之前的优先级,以同步优先级执行,执行完毕后恢复之前优先级
    const previousPriority = getCurrentUpdatePriority();
    setCurrentUpdatePriority(DiscreteEventPriority);
    
    // 将当前上下文标记为CommitContext,作为commit阶段的标志
    const prevExecutionContext = executionContext;
    executionContext |= CommitContext;

    ReactCurrentOwner.current = null;
     
    // beforeMutation 阶段的主函数
    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );
    
    .....
    

before Mutation 阶段的主函数是 commitBeforeMutationEffects

可以在 这里 看到源码

export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  
  // 遍历effectList
  commitBeforeMutationEffects_begin();

  const shouldFire = shouldFireAfterActiveInstanceBlur;
  
  // 处理focus状态
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

commitBeforeMutationEffects_begin

function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;

    
    const deletions = fiber.deletions;
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const deletion = deletions[i];
        commitBeforeMutationEffectsDeletion(deletion);
      }
    }

    const child = fiber.child;
    if (
      (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
      child !== null
    ) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      commitBeforeMutationEffects_complete();
    }
  }
}

fiber.subtreeFlags 表示子树包含的副作用标识,避免深度遍历。当所有带有副作用的 fiber 节点都遍历完后,会执行 commitBeforeMutationEffects_complete 。

commitBeforeMutationEffects_complete

commitBeforeMutationEffects_complete 方法中对于每个 fiber 节点都会调用 commitBeforeMutationEffectsOnFiber 方法。

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if (__DEV__) {
     .....
     
    } else {
      try {
        commitBeforeMutationEffectsOnFiber(fiber);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

commitBeforeMutationEffectsOnFiber

commitBeforeMutationEffectsOnFiber 方法中会对于 ClassComponent 会调用 getSnapshotBeforeUpdate 生命周期方法。可以在 这里 看到源码。

function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
  const current = finishedWork.alternate;
  const flags = finishedWork.flags;

  if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
    if (
      finishedWork.tag === SuspenseComponent &&
      isSuspenseBoundaryBeingHidden(current, finishedWork) &&
      doesFiberContain(finishedWork, focusedInstanceHandle)
    ) {
      shouldFireAfterActiveInstanceBlur = true;
      beforeActiveInstanceBlur(finishedWork);
    }
  }

  if ((flags & Snapshot) !== NoFlags) {
    setCurrentDebugFiberInDEV(finishedWork);

    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        break;
      }
      case ClassComponent: {
        if (current !== null) {
          const prevProps = current.memoizedProps;
          const prevState = current.memoizedState;
          const instance = finishedWork.stateNode;
          
          instance.__reactInternalSnapshotBeforeUpdate = snapshot;
        }
        break;
      }
      case HostRoot: {
        if (supportsMutation) {
          const root = finishedWork.stateNode;
          clearContainer(root.containerInfo);
        }
        break;
      }
      case HostComponent:
      case HostText:
      case HostPortal:
      case IncompleteClassComponent:
        // Nothing to do for these component types
        break;
      default: {
        invariant(
          false,
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
      }
    }

    resetCurrentDebugFiberInDEV();
  }
}

getSnapshotBeforeUpdate

getSnapshotBeforeUpdate(prevProps, prevState)

getSnapshotBeforeUpdate() 在最近一次渲染输出(提交到 DOM 节点)之前调用。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)。此生命周期方法的任何返回值将作为参数传递给 componentDidUpdate()。

v16.3.0 开始 componentWillMountcomponentWillRecievePropscomponentWillUpdate 三个生命周期钩子被标记为 UNSAFE

被标记为 UNSAFE 是因为 Stack Reconciler 重构为 Fiber Reconciler 后,render 阶段的任务可能中断/重新开始,componentWillXXX 生命周期钩子可能触发多次。

getSnapshotBeforeUpdate 是在 commit 阶段内的 before mutation 阶段调用的,由于commit 阶段是同步的,只会调用一次。

mutation 阶段

mutation 阶段的主函数是 commitMutationEffects。

源码位置: github.com/facebook/re…

commitMutationEffects(root, finishedWork, lanes);

其中参数 root 代表了整个应用的根节点 FiberRootNode,finishedWork 代表是所在组件树的根节点 rootFiber,lanes 表示优先级相关。

commitMutationEffects

你可以在 这里 看到 commitMutationEffects 源码

export function commitMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = firstChild;

  commitMutationEffects_begin(root);

  inProgressLanes = null;
  inProgressRoot = null;
}

从上面的代码中我们可以看到该方法会调用 commitMutationEffects_begin 方法。

commitMutationEffects_begin

function commitMutationEffects_begin(root: FiberRoot) {
  // 遍历 effectList
  while (nextEffect !== null) {
    const fiber = nextEffect;

    const deletions = fiber.deletions;
    if (deletions !== null) {
      for (let i = 0; i < deletions.length; i++) {
        const childToDelete = deletions[i];
        if (__DEV__) {
        
          ....
          
        } else {
          try {
            commitDeletion(root, childToDelete, fiber);
          } catch (error) {
            captureCommitPhaseError(childToDelete, fiber, error);
          }
        }
      }
    }

    const child = fiber.child;
    if ((fiber.subtreeFlags & MutationMask) !== NoFlags && child !== null) {
      ensureCorrectReturnPointer(child, fiber);
      nextEffect = child;
    } else {
      // 遍历完成
      commitMutationEffects_complete(root);
    }
  }
}

commitMutationEffects_begin 会遍历带有 effectTag 的 effectList。当 Fiber 节点含有 Deletion effectTag,调用 commitDeletion 方法删除。

Deletion effect

function commitDeletion(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  if (supportsMutation) {
   
    unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
    
  } else {
   
    commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
    
  }

  detachFiberMutation(current);
}

最终会调用有 commitUnmount 方法

可以从 这里 看到源码

function commitUnmount(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  onCommitUnmount(current);

  switch (current.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      const updateQueue: FunctionComponentUpdateQueue | null = (current.updateQueue: any);
      if (updateQueue !== null) {
        const lastEffect = updateQueue.lastEffect;
        if (lastEffect !== null) {
          const firstEffect = lastEffect.next;

          let effect = firstEffect;
          do {
            const {destroy, tag} = effect;
            if (destroy !== undefined) {
              if ((tag & HookLayout) !== NoHookEffect) {
                if (
                  enableProfilerTimer &&
                  enableProfilerCommitHooks &&
                  current.mode & ProfileMode
                ) {
                  startLayoutEffectTimer();
                  safelyCallDestroy(current, nearestMountedAncestor, destroy);
                  recordLayoutEffectDuration(current);
                } else {
                  safelyCallDestroy(current, nearestMountedAncestor, destroy);
                }
              }
            }
            effect = effect.next;
          } while (effect !== firstEffect);
        }
      }
      return;
    }
    case ClassComponent: {
      safelyDetachRef(current, nearestMountedAncestor);
      const instance = current.stateNode;
      if (typeof instance.componentWillUnmount === 'function') {
        safelyCallComponentWillUnmount(
          current,
          nearestMountedAncestor,
          instance,
        );
      }
      return;
    }
    case HostComponent: {
      safelyDetachRef(current, nearestMountedAncestor);
      return;
    }
    case HostPortal: {
      if (supportsMutation) {
        unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
      } else if (supportsPersistence) {
        emptyPortalContainer(current);
      }
      return;
    }
    case DehydratedFragment: {
      if (enableSuspenseCallback) {
        const hydrationCallbacks = finishedRoot.hydrationCallbacks;
        if (hydrationCallbacks !== null) {
          const onDeleted = hydrationCallbacks.onDeleted;
          if (onDeleted) {
            onDeleted((current.stateNode: SuspenseInstance));
          }
        }
      }
      return;
    }
    case ScopeComponent: {
      if (enableScopeAPI) {
        safelyDetachRef(current, nearestMountedAncestor);
      }
      return;
    }
  }
}

该方法主要工作:

  • 递归调用 Fiber 节点及其子孙 Fiber 节点中 fiber.tag 为 ClassComponent 的componentWillUnmount 生命周期钩子,从页面移除Fiber节点对应DOM节点;

  • 解绑 ref 属性;

  • 调度 FunctionComponent useEffect 的销毁函数;

当所有带有副作用的 fiber 节点都遍历完后,会执行 commitMutationEffects_complete 方法。

commitMutationEffects_complete

function commitMutationEffects_complete(root: FiberRoot) {
   // 遍历effectList
  while (nextEffect !== null) {
    const fiber = nextEffect;
    if (__DEV__) {
    
      .....
      
    } else {
      try {
        commitMutationEffectsOnFiber(fiber, root);
      } catch (error) {
        captureCommitPhaseError(fiber, fiber.return, error);
      }
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

在 commitMutationEffects_complete 方法中会执行 commitMutationEffectsOnFiber 方法。

commitMutationEffectsOnFiber

源码:github.com/facebook/re…

function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  
   // 根据 ContentReset effectTag重置文字节点
  if (flags & ContentReset) {
    commitResetTextContent(finishedWork);
  }
  
  // 更新ref
  if (flags & Ref) {
    const current = finishedWork.alternate;
    if (current !== null) {
      commitDetachRef(current);
    }
    if (enableScopeAPI) {
      if (finishedWork.tag === ScopeComponent) {
        commitAttachRef(finishedWork);
      }
    }
  }
  
  // 根据 effectTag 分别处理
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    // 插入DOM
    case Placement: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      break;
    }
    // 插入DOM 并 更新DOM
    case PlacementAndUpdate: {
      // Placement
      commitPlacement(finishedWork);
     
      finishedWork.flags &= ~Placement;

      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    // SSR 相关
    case Hydrating: {
      finishedWork.flags &= ~Hydrating;
      break;
    }
    // SSR 相关
    case HydratingAndUpdate: {
      finishedWork.flags &= ~Hydrating;

      // Update
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
    // 更新 DOM
    case Update: {
      const current = finishedWork.alternate;
      commitWork(current, finishedWork);
      break;
    }
  }
}

commitMutationEffectsOnFiber 对每个带有 effectTag 的 Fiber 节点执行如下三个操作:

  1. 根据 ContentReset effectTag 重置文字节点;
  2. 更新 ref 属性;
  3. 根据 effectTag 分别处理,其中 effectTag 包括(Placement | Update | Hydrating | HydratingAndUpdate)

Placement effect

当 Fiber节点含有 Placement effectTag,表示该 Fiber 节点对应的 DOM 节点需要插入到页面中,会调用的 commitPlacement 方法。

commitPlacement 方法 源码

function commitPlacement(finishedWork: Fiber): void {
  if (!supportsMutation) {
    return;
  }

  const parentFiber = getHostParentFiber(finishedWork);

 
  let parent;
  let isContainer;
  const parentStateNode = parentFiber.stateNode;
  switch (parentFiber.tag) {
    case HostComponent:
      parent = parentStateNode;
      isContainer = false;
      break;
    case HostRoot:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    case HostPortal:
      parent = parentStateNode.containerInfo;
      isContainer = true;
      break;
    default:
      invariant(
        false,
        'Invalid host parent fiber. This error is likely caused by a bug ' +
          'in React. Please file an issue.',
      );
  }
  if (parentFiber.flags & ContentReset) {
   
    resetTextContent(parent);
   
    parentFiber.flags &= ~ContentReset;
  }

  const before = getHostSibling(finishedWork);
 
  if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

该方法所做的工作分为三步:

  1. 获取父级 DOM 节点
const parentFiber = getHostParentFiber(finishedWork);
  1. 获取 Fiber 节点的 DOM 兄弟节点
const before = getHostSibling(finishedWork);
  1. 根据 parentStateNode 是否是 rootFiber 来调用 parentNode.insertBefore 或者parentNode.appendChild 执行 DOM 插入操作。
if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
  • insertOrAppendPlacementNodeIntoContainer 方法
function insertOrAppendPlacementNodeIntoContainer(
  node: Fiber,
  before: ?Instance,
  parent: Container,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertInContainerBefore(parent, stateNode, before);
    } else {
      appendChildToContainer(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // If the insertion itself is a portal, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else {
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNodeIntoContainer(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

  • insertOrAppendPlacementNode 方法
function insertOrAppendPlacementNode(
  node: Fiber,
  before: ?Instance,
  parent: Instance,
): void {
  const {tag} = node;
  const isHost = tag === HostComponent || tag === HostText;
  if (isHost) {
    const stateNode = node.stateNode;
    if (before) {
      insertBefore(parent, stateNode, before);
    } else {
      appendChild(parent, stateNode);
    }
  } else if (tag === HostPortal) {
    // If the insertion itself is a portal, then we don't want to traverse
    // down its children. Instead, we'll get insertions from each child in
    // the portal directly.
  } else {
    const child = node.child;
    if (child !== null) {
      insertOrAppendPlacementNode(child, before, parent);
      let sibling = child.sibling;
      while (sibling !== null) {
        insertOrAppendPlacementNode(sibling, before, parent);
        sibling = sibling.sibling;
      }
    }
  }
}

这两个方法都会根据 Fiber 节点的 DOM 兄弟节点是否存在来执行 parentNode.appendChild 方法或者 parentNode.insertBefore 方法。

Update effect

当 Fiber 节点含有 Update effectTag,意味着该 Fiber 节点需要更新,此时调用 commitWork 方法。

commitWork 源码

function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  .......

  switch (finishedWork.tag) {
    case FunctionComponent:
    case ForwardRef:
    case MemoComponent:
    case SimpleMemoComponent: {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          commitHookEffectListUnmount(
            HookLayout | HookHasEffect,
            finishedWork,
            finishedWork.return,
          );
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        commitHookEffectListUnmount(
          HookLayout | HookHasEffect,
          finishedWork,
          finishedWork.return,
        );
      }
      return;
    }
    case ClassComponent: {
      return;
    }
    case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
        // Commit the work prepared earlier.
        const newProps = finishedWork.memoizedProps;
       
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
     
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }
    case HostText: {
      invariant(
        finishedWork.stateNode !== null,
        'This should have a text node initialized. This error is likely ' +
          'caused by a bug in React. Please file an issue.',
      );
      const textInstance: TextInstance = finishedWork.stateNode;
      const newText: string = finishedWork.memoizedProps;
    
      const oldText: string =
        current !== null ? current.memoizedProps : newText;
      commitTextUpdate(textInstance, oldText, newText);
      return;
    }
    case HostRoot: {
      if (supportsHydration) {
        const root: FiberRoot = finishedWork.stateNode;
        if (root.hydrate) {
          // We've just hydrated. No need to hydrate again.
          root.hydrate = false;
          commitHydratedContainer(root.containerInfo);
        }
      }
      return;
    }
    case Profiler: {
      return;
    }
    case SuspenseComponent: {
      commitSuspenseComponent(finishedWork);
      attachSuspenseRetryListeners(finishedWork);
      return;
    }
    case SuspenseListComponent: {
      attachSuspenseRetryListeners(finishedWork);
      return;
    }
    case IncompleteClassComponent: {
      return;
    }
    case ScopeComponent: {
      if (enableScopeAPI) {
        const scopeInstance = finishedWork.stateNode;
        prepareScopeUpdate(scopeInstance, finishedWork);
        return;
      }
      break;
    }
    case OffscreenComponent:
    case LegacyHiddenComponent: {
      const newState: OffscreenState | null = finishedWork.memoizedState;
      const isHidden = newState !== null;
      hideOrUnhideAllChildren(finishedWork, isHidden);
      return;
    }
  }
  invariant(
    false,
    'This unit of work tag should not have side-effects. This error is ' +
      'likely caused by a bug in React. Please file an issue.',
  );
}
  • FunctionComponent

当 fiber.tag 为 FunctionComponent 时,会调用 commitHookEffectListUnmount 方法。该方法会遍历 effectList ,执行所有 useLayoutEffect hook 的销毁函数。

commitHookEffectListUnmount 源码

function commitHookEffectListUnmount(
  flags: HookFlags,
  finishedWork: Fiber,
  nearestMountedAncestor: Fiber | null,
) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & flags) === flags) {
        // Unmount
        const destroy = effect.destroy;
        effect.destroy = undefined;
        if (destroy !== undefined) {
          safelyCallDestroy(finishedWork, nearestMountedAncestor, destroy);
        }
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}
  • HostComponent

当 fiber.tag 为 HostComponent 时,会调用 commitUpdate 方法。

case HostComponent: {
      const instance: Instance = finishedWork.stateNode;
      if (instance != null) {
       
        const newProps = finishedWork.memoizedProps;
        
        const oldProps = current !== null ? current.memoizedProps : newProps;
        const type = finishedWork.type;
       
        const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
        
        finishedWork.updateQueue = null;
        if (updatePayload !== null) {
          commitUpdate(
            instance,
            updatePayload,
            type,
            oldProps,
            newProps,
            finishedWork,
          );
        }
      }
      return;
    }

commitUpdate 源码

export function commitUpdate(
  domElement: Instance,
  updatePayload: Array<mixed>,
  type: string,
  oldProps: Props,
  newProps: Props,
  internalInstanceHandle: Object,
): void {
  
  updateFiberProps(domElement, newProps);
 
 
  updateProperties(domElement, updatePayload, type, oldProps, newProps);
  
}

可以看出 commitUpdate 会调用 updateProperties 方法更新 DOM 属性。

layout 阶段

layout 阶段的代码都是在 DOM 渲染完成( mutation 阶段完成)后执行的,在执行 layout 阶段的代码之前会执行 root.current = finishedWork;

可以从 这里 看到源码

   // The work-in-progress tree is now the current tree. This must come after
    // the mutation phase, so that the previous tree is still current during
    // componentWillUnmount, but before the layout phase, so that the finished
    // work is current during componentDidMount/Update.
    root.current = finishedWork;

finishedWork 表示已经渲染完成的 workInProgress Fiber 树,根据 Fiber 架构双缓存机制,fiberRootNode 的 current 指针会指向 workInProgress Fiber 树,意味着 workInProgress Fiber 树变成了 current Fiber 树

在 mutation 阶段之后 layout 阶段之前执行 root.current = finishedWork 是因为在 mutation 阶段会执行 componentWillUnmount 生命周期,此时 root.current 还指向之前的 Fiber 树。而在 layout 阶段会执行 componentDidMount/Update 两个生命周期函数,此时 root.current 已经指向了 workInProgress Fiber 树。

commitLayoutEffects

layout 阶段会调用 commitLayoutEffects 函数。

可以从 这里 看到源码

export function commitLayoutEffects(
  finishedWork: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
): void {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  nextEffect = finishedWork;

  commitLayoutEffects_begin(finishedWork, root, committedLanes);

  inProgressLanes = null;
  inProgressRoot = null;
}

在该方法中会执行 commitLayoutEffects_begin

commitLayoutEffects_begin

可以从 这里 看到源码

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;
     
     // Offscreen是一个开发中的API,预计会在某个v18的小版本发布。
     // 他的功能类似Vue中的keep-alive,用来在组件「失活」时在后台保存组件状态。
     
    if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
      // Keep track of the current Offscreen stack's state.
      if (fiber.tag === OffscreenComponent) {
        const current = fiber.alternate;
        const wasHidden = current !== null && current.memoizedState !== null;
        const isHidden = fiber.memoizedState !== null;

        const newOffscreenSubtreeIsHidden =
          isHidden || offscreenSubtreeIsHidden;
        const newOffscreenSubtreeWasHidden =
          wasHidden || offscreenSubtreeWasHidden;

        if (
          newOffscreenSubtreeIsHidden !== offscreenSubtreeIsHidden ||
          newOffscreenSubtreeWasHidden !== offscreenSubtreeWasHidden
        ) {
          const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
          const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;

          // Traverse the Offscreen subtree with the current Offscreen as the root.
          offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
          offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
          commitLayoutEffects_begin(
            fiber, // New root; bubble back up to here and stop.
            root,
            committedLanes,
          );

          // Restore Offscreen state and resume in our-progress traversal.
          nextEffect = fiber;
          offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
          offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
          commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);

          continue;
        }
      }
    }

    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
        const visibilityChanged =
          !offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;

        // TODO (Offscreen) Also check: subtreeFlags & LayoutStatic
        if (visibilityChanged && firstChild !== null) {
          // We've just shown or hidden a Offscreen tree that contains layout effects.
          // We only enter this code path for subtrees that are updated,
          // because newly mounted ones would pass the LayoutMask check above.
          ensureCorrectReturnPointer(firstChild, fiber);
          nextEffect = firstChild;
          continue;
        }
      }

      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

fiber.tag === OffscreenComponent 这个条件 React 是一个开发中的API,预计会在某个 v18 发布。他的功能类似Vue中的keep-alive,用来在组件「失活」时在后台保存组件状态,暂时不管他。

function commitLayoutEffects_begin(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    const fiber = nextEffect;
    const firstChild = fiber.child;

    ...

    if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
      ensureCorrectReturnPointer(firstChild, fiber);
      nextEffect = firstChild;
    } else {
      if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
        const visibilityChanged =
          !offscreenSubtreeIsHidden && offscreenSubtreeWasHidden;

        if (visibilityChanged && firstChild !== null) {
          ensureCorrectReturnPointer(firstChild, fiber);
          nextEffect = firstChild;
          continue;
        }
      }

      commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
    }
  }
}

commitLayoutEffects_begin 方法会从上往下遍历 effectList ,最终会执行 commitLayoutMountEffects_complete 方法。

commitLayoutMountEffects_complete

function commitLayoutMountEffects_complete(
  subtreeRoot: Fiber,
  root: FiberRoot,
  committedLanes: Lanes,
) {
  // Suspense layout effects semantics don't change for legacy roots.
  const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;

  while (nextEffect !== null) {
    const fiber = nextEffect;

    if (
      enableSuspenseLayoutEffectSemantics &&
      isModernRoot &&
      offscreenSubtreeWasHidden &&
      !offscreenSubtreeIsHidden
    ) {
      switch (fiber.tag) {
        case FunctionComponent:
        case ForwardRef:
        case SimpleMemoComponent: {
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            fiber.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
            } finally {
              recordLayoutEffectDuration(fiber);
            }
          } else {
            safelyCallCommitHookLayoutEffectListMount(fiber, fiber.return);
          }
          break;
        }
        case ClassComponent: {
          const instance = fiber.stateNode;
          if (typeof instance.componentDidMount === 'function') {
            safelyCallComponentDidMount(fiber, fiber.return, instance);
          }
          break;
        }
      }

      // TODO (Offscreen) Check flags & RefStatic
      switch (fiber.tag) {
        case ClassComponent:
        case HostComponent:
          safelyAttachRef(fiber, fiber.return);
          break;
      }
    } else if ((fiber.flags & LayoutMask) !== NoFlags) {
      const current = fiber.alternate;
      if (__DEV__) {
        setCurrentDebugFiberInDEV(fiber);
        invokeGuardedCallback(
          null,
          commitLayoutEffectOnFiber,
          null,
          root,
          current,
          fiber,
          committedLanes,
        );
        if (hasCaughtError()) {
          const error = clearCaughtError();
          captureCommitPhaseError(fiber, fiber.return, error);
        }
        resetCurrentDebugFiberInDEV();
      } else {
        try {
          commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
        } catch (error) {
          captureCommitPhaseError(fiber, fiber.return, error);
        }
      }
    }

    if (fiber === subtreeRoot) {
      nextEffect = null;
      return;
    }

    const sibling = fiber.sibling;
    if (sibling !== null) {
      ensureCorrectReturnPointer(sibling, fiber.return);
      nextEffect = sibling;
      return;
    }

    nextEffect = fiber.return;
  }
}

根据 nextEffect = fiber.return 这段代码可以看出 commitLayoutMountEffects_complete 会从下往上 遍历 effectList 。在该方法中会执行 commitLayoutEffectOnFiber 方法。

commitLayoutEffectOnFiber

commitLayoutEffectOnFiber 一共做了两件事:

  1. 调用生命周期钩子和 hook 相关操作
  2. commitAttachRef(赋值 ref)

commitLayoutEffectOnFiber 源码

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & (Update | Callback)) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (
          enableProfilerTimer &&
          enableProfilerCommitHooks &&
          finishedWork.mode & ProfileMode
        ) {
          try {
            startLayoutEffectTimer();
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          } finally {
            recordLayoutEffectDuration(finishedWork);
          }
        } else {
          commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
        }
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (current === null) {
            
            if (
              enableProfilerTimer &&
              enableProfilerCommitHooks &&
              finishedWork.mode & ProfileMode
            ) {
              try {
                startLayoutEffectTimer();
                instance.componentDidMount();
              } finally {
                recordLayoutEffectDuration(finishedWork);
              }
            } else {
              instance.componentDidMount();
            }
          } else {
            const prevProps =
              finishedWork.elementType === finishedWork.type
                ? current.memoizedProps
                : resolveDefaultProps(finishedWork.type, current.memoizedProps);
            const prevState = current.memoizedState;
          
           
            if (
              enableProfilerTimer &&
              enableProfilerCommitHooks &&
              finishedWork.mode & ProfileMode
            ) {
              try {
                startLayoutEffectTimer();
                instance.componentDidUpdate(
                  prevProps,
                  prevState,
                  instance.__reactInternalSnapshotBeforeUpdate,
                );
              } finally {
                recordLayoutEffectDuration(finishedWork);
              }
            } else {
              instance.componentDidUpdate(
                prevProps,
                prevState,
                instance.__reactInternalSnapshotBeforeUpdate,
              );
            }
          }
        }

        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
        
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }
      case HostComponent: {
        const instance: Instance = finishedWork.stateNode;

     
        if (current === null && finishedWork.flags & Update) {
          const type = finishedWork.type;
          const props = finishedWork.memoizedProps;
          commitMount(instance, type, props, finishedWork);
        }

        break;
      }
      case HostText: {
        // We have no life-cycles associated with text.
        break;
      }
      case HostPortal: {
        // We have no life-cycles associated with portals.
        break;
      }
      case Profiler: {
        if (enableProfilerTimer) {
          const {onCommit, onRender} = finishedWork.memoizedProps;
          const {effectDuration} = finishedWork.stateNode;

          const commitTime = getCommitTime();

          let phase = current === null ? 'mount' : 'update';
          if (enableProfilerNestedUpdatePhase) {
            if (isCurrentUpdateNested()) {
              phase = 'nested-update';
            }
          }

          if (typeof onRender === 'function') {
            onRender(
              finishedWork.memoizedProps.id,
              phase,
              finishedWork.actualDuration,
              finishedWork.treeBaseDuration,
              finishedWork.actualStartTime,
              commitTime,
            );
          }

          if (enableProfilerCommitHooks) {
            if (typeof onCommit === 'function') {
              onCommit(
                finishedWork.memoizedProps.id,
                phase,
                effectDuration,
                commitTime,
              );
            }

          
            enqueuePendingPassiveProfilerEffect(finishedWork);

           
            let parentFiber = finishedWork.return;
            outer: while (parentFiber !== null) {
              switch (parentFiber.tag) {
                case HostRoot:
                  const root = parentFiber.stateNode;
                  root.effectDuration += effectDuration;
                  break outer;
                case Profiler:
                  const parentStateNode = parentFiber.stateNode;
                  parentStateNode.effectDuration += effectDuration;
                  break outer;
              }
              parentFiber = parentFiber.return;
            }
          }
        }
        break;
      }
      case SuspenseComponent: {
        commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
        break;
      }
      case SuspenseListComponent:
      case IncompleteClassComponent:
      case ScopeComponent:
      case OffscreenComponent:
      case LegacyHiddenComponent:
        break;
      default:
        invariant(
          false,
          'This unit of work tag should not have side-effects. This error is ' +
            'likely caused by a bug in React. Please file an issue.',
        );
    }
  }

  if (enableScopeAPI) {

    if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
      commitAttachRef(finishedWork);
    }
  } else {
    if (finishedWork.flags & Ref) {
      commitAttachRef(finishedWork);
    }
  }
}

FunctionComponent

function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & (Update | Callback)) !== NoFlags) {
        switch (finishedWork.tag) {
           case FunctionComponent:
           case ForwardRef:
           case SimpleMemoComponent: {
             if (
             enableProfilerTimer &&
             enableProfilerCommitHooks &&
             finishedWork.mode & ProfileMode) {
               try {
                startLayoutEffectTimer();
                commitHookEffectListMount(HookLayout | HookHasEffect,finishedWork);
               } finally {
                recordLayoutEffectDuration(finishedWork);
             }
            } else {
            commitHookEffectListMount(HookLayout |  HookHasEffect,finishedWork);
           }
          break;
         }
         ....
       }
    }
}

commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork); 会执行 useLayoutEffect 的回调函数。

function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
  const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
  const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
  if (lastEffect !== null) {
    const firstEffect = lastEffect.next;
    let effect = firstEffect;
    do {
      if ((effect.tag & tag) === tag) {
        // Mount 执行 useLayoutEffect 回调函数
        const create = effect.create;
        effect.destroy = create();
      }
      effect = effect.next;
    } while (effect !== firstEffect);
  }
}

在 mutation 阶段会执行上一次 useLayoutEffect hook 的销毁函数,在 layout 阶段会调用 useLayoutEffect hook 本次更新的回调函数,是同步调用的。

ClassComponent

对于 ClassComponent 会根据 current 是否存在(current === null) 来调用 instance.componentDidMount() 或者 instance.componentDidUpdate() 生命周期函数。

HostRoot

HostRoot,即 rootFiber,如果赋值了第三个参数回调函数,也会在此时调用。

ReactDOM.render(<App />, document.getElementById('root'),()=>{
    console.log('ReactDOM.render 回调');
});
case HostRoot: {
        const updateQueue: UpdateQueue<
          *,
        > | null = (finishedWork.updateQueue: any);
        if (updateQueue !== null) {
          let instance = null;
          if (finishedWork.child !== null) {
            switch (finishedWork.child.tag) {
              case HostComponent:
                instance = getPublicInstance(finishedWork.child.stateNode);
                break;
              case ClassComponent:
                instance = finishedWork.child.stateNode;
                break;
            }
          }
          // 执行 ReactDOM.render 第三个参数
          commitUpdateQueue(finishedWork, updateQueue, instance);
        }
        break;
      }

commitUpdateQueue 函数

export function commitUpdateQueue<State>(
  finishedWork: Fiber,
  finishedQueue: UpdateQueue<State>,
  instance: any,
): void {
  // Commit the effects
  const effects = finishedQueue.effects;
  finishedQueue.effects = null;
  if (effects !== null) {
    for (let i = 0; i < effects.length; i++) {
      const effect = effects[i];
      const callback = effect.callback;
      if (callback !== null) {
        effect.callback = null;
        callCallback(callback, instance);
      }
    }
  }
}

commitAttachRef

if (enableScopeAPI) {
  
    if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
      commitAttachRef(finishedWork);
    }
  } else {
    if (finishedWork.flags & Ref) {
      commitAttachRef(finishedWork);
    }
  }

commitAttachRef

function commitAttachRef(finishedWork: Fiber) {
  const ref = finishedWork.ref;
  if (ref !== null) {
    // 获取DOM实例
    const instance = finishedWork.stateNode;
    let instanceToUse;
    switch (finishedWork.tag) {
      case HostComponent:
        instanceToUse = getPublicInstance(instance);
        break;
      default:
        instanceToUse = instance;
    }
    // Moved outside to ensure DCE works with this flag
    if (enableScopeAPI && finishedWork.tag === ScopeComponent) {
      instanceToUse = instance;
    }
    if (typeof ref === 'function') {
      if (
        enableProfilerTimer &&
        enableProfilerCommitHooks &&
        finishedWork.mode & ProfileMode
      ) {
        try {
          startLayoutEffectTimer();
          ref(instanceToUse);
        } finally {
          recordLayoutEffectDuration(finishedWork);
        }
      } else {
        ref(instanceToUse);
      }
    } else {
      

      ref.current = instanceToUse;
    }
  }
}