React 源码阅读 - 渲染

649 阅读8分钟

什么是渲染阶段

渲染的工作由渲染器 (Renderer)来完成,这个阶段被称作 commit 阶段,这个阶段将会把提交的信息渲染到页面上,也就是上一个阶段我们”协调“了半天的 Fiber

基本流程

从流程上来说 commit 阶段可以分为三个阶段:

  • before mutation 阶段,这个阶段 DOM 节点还没有被渲染到界面上去,过程中会触发 getSnapshotBeforeUpdate,也会处理 useEffect 钩子相关的调度逻辑
  • mutation 阶段,这个阶段负责 DOM 节点的渲染,在渲染过程中会根据协调阶段的标记(flags),执行不同的 DOM 操作
  • layout 阶段,这个阶段处理 DOM 渲染完毕之后的收尾逻辑,比如:componentDidMount/componentDidUpdate,调用 useLayoutEffect 钩子函数的回调等,还会把 fiberRootcurrent 指针指向 workInProgress Fiber

源码分析

与协调阶段一样,渲染阶段的起点也在 performSyncWorkOnRoot 函数,这里的关键调用就是 commitRoot 函数,这里的入参 root 就是协调阶段处理好的 FiberRootNode,是整个应用的根 Fiber,而 finishedWork 则是我们在协调阶段在内存中构建好的 Work In Progress

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function performSyncWorkOnRoot(root) {
  // 省略一些代码

  if (exitStatus === RootFatalErrored) {
    const fatalError = workInProgressRootFatalError;
    // 协调阶段
    prepareFreshStack(root, NoLanes);
    markRootSuspended(root, lanes);
    ensureRootIsScheduled(root, now());
    throw fatalError;
  }

  const finishedWork: Fiber = (root.current.alternate: any);
  root.finishedWork = finishedWork;
  root.finishedLanes = lanes;
  // 渲染阶段
  commitRoot(root);

  ensureRootIsScheduled(root, now());

  return null;
}

commitRoot 又主要调用 commitRootImpl 函数,这个函数内容比较多,主要分为三个阶段,也就是基本流程中介绍的三个阶段,下面一个一个说。

整体调用过程如下图所示:

React-commit阶段.png

在进入before mutation 阶段之前,有一个比较重要的调用,如下面代码所示:

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
  do {
    flushPassiveEffects();
  } while (rootWithPendingPassiveEffects !== null);
  // 省略一些代码

  // 获取到内存中处理好的最新的 fiber
  const finishedWork = root.finishedWork;
    
  // 省略一些代码
  if (
    (finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
    (finishedWork.flags & PassiveMask) !== NoFlags
  ) {
    if (!rootDoesHavePassiveEffects) {
      rootDoesHavePassiveEffects = true;
      // 异步调用 useEffect
      scheduleCallback(NormalSchedulerPriority, () => {
        flushPassiveEffects();
        return null;
      });
    }
  }
  
  // 省略一些代码
  if (subtreeHasEffects || rootHasEffect) {
    // 省略一些代码

    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );

    // 省略一些代码
}

上面这一段代码包含了 before mutation 阶段之前主要做的事情,scheduleCallback 异步调用了 flushPassiveEffects(页面绘制完成执行),这个函数的调用栈也很深,这里就不详细展开了,最终会调用 useEffect

before mutation

commitBeforeMutationEffectsbefore mutation 阶段的重要入口。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
export function commitBeforeMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
) {
  focusedInstanceHandle = prepareForCommit(root.containerInfo);

  nextEffect = firstChild;
  commitBeforeMutationEffects_begin();

  // We no longer need to track the active instance fiber
  const shouldFire = shouldFireAfterActiveInstanceBlur;
  shouldFireAfterActiveInstanceBlur = false;
  focusedInstanceHandle = null;

  return shouldFire;
}

可以看到这里 nextEffect 被赋值,这是所有 Fiber 的起点,准备好了 nextEffect,就要开始用了。 随后便调用了 commitBeforeMutationEffects_begin 函数。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitBeforeMutationEffects_begin() {
  while (nextEffect !== null) {
    const fiber = nextEffect;

    if (enableCreateEventHandleAPI) {
      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();
    }
  }
}

function commitBeforeMutationEffects_complete() {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    setCurrentDebugFiberInDEV(fiber);
    try {
      commitBeforeMutationEffectsOnFiber(fiber);
    } catch (error) {
      reportUncaughtErrorInDEV(error);
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    resetCurrentDebugFiberInDEV();

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

    nextEffect = fiber.return;
  }
}

不难看出来,此处的调用和协调阶段是比像的,也是 begin 的时候深度优先遍历 child 节点,然后 complete 遍历 sibling 节点。commitBeforeMutationEffects_complete 函数会调用 commitBeforeMutationEffectsOnFiber 函数,此函数中也有一堆 switch...case... 语句,class 组件的时候会调用 getSnapshotBeforeUpdate 函数。

mutation

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
  // 省略一些代码
  if (subtreeHasEffects || rootHasEffect) {
    // 省略一些代码

    const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
      root,
      finishedWork,
    );
      
    // 省略一些代码
    commitMutationEffects(root, finishedWork, lanes);
    // 省略一些代码
  }
  // 省略一些代码
}

mutation 阶段的入口函数是 commitMutationEffects,入参:

  • root 是 FiberRoot 类型,是根 Fiber
  • finishedWork 是 Fiber 类型,是前面阶段处理过的
  • lanes 是 Lanes 类型,与赛道相关的,暂时不深入讲
// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
export function commitMutationEffects(
  root: FiberRoot,
  firstChild: Fiber,
  committedLanes: Lanes,
) {
  inProgressLanes = committedLanes;
  inProgressRoot = root;
  // 用于驱动循环的起点,commitMutationEffects_begin 函数中会用到
  nextEffect = firstChild;

  commitMutationEffects_begin(root);

  inProgressLanes = null;
  inProgressRoot = null;
}

commitMutationEffects 又调用了 commitMutationEffects_begin,它又调用了 commitMutationEffects_complete

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffects_begin(root: FiberRoot) {
  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];
        try {
          commitDeletion(root, childToDelete, fiber);
        } catch (error) {
          reportUncaughtErrorInDEV(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);
    }
  }
}

function commitMutationEffects_complete(root: FiberRoot) {
  while (nextEffect !== null) {
    const fiber = nextEffect;
    setCurrentDebugFiberInDEV(fiber);
    try {
      commitMutationEffectsOnFiber(fiber, root);
    } catch (error) {
      reportUncaughtErrorInDEV(error);
      captureCommitPhaseError(fiber, fiber.return, error);
    }
    resetCurrentDebugFiberInDEV();

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

    nextEffect = fiber.return;
  }
}

其实这两个函数做的事情与前面阶段做的事情也类似,都是先遍历 Fiber树的 child 节点,再遍历其兄弟节点,commitMutationEffects_complete 又调用了commitMutationEffectsOnFiber,过程中会处理各种标记(Placement | Update | Deletion | Hydrating),Hydrating 是服务端渲染相关的,先不考虑。前面几个分别插入、更新和删除操作。

进入 commitMutationEffects_begin 首先是判断 deletions 标记,看是否需要调用 commitDeletion

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitDeletion(
  finishedRoot: FiberRoot,
  current: Fiber,
  nearestMountedAncestor: Fiber,
): void {
  if (supportsMutation) {
    unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
  } else {
    commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
  }

  detachFiberMutation(current);
}

commitDeletion 会执行如下操作:

  1. 递归调用 Fiber 节点及其子孙 Fiber 节点,fiber.tagClassComponent 的会调用 componentWillUnmount 生命周期钩子,从页面移除 Fiber 对应的 DOM 节点
  2. 解绑 ref
  3. 调度 useLayoutEffect 的销毁函数
// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
  const flags = finishedWork.flags;
  
  // 省略一些代码
  // 根据 ContentReset flags 重置文字节点
  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);
      }
    }
  }
  
  const primaryFlags = flags & (Placement | Update | Hydrating);
  outer: switch (primaryFlags) {
    case Placement: {
      commitPlacement(finishedWork);
      finishedWork.flags &= ~Placement;
      break;
    }
    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 函数做的事情基本上就是 mutation 阶段主要做的事情:

  1. commitResetTextContent 最终调用了 setTextContent,重置了文本类型节点
  2. commitDetachRefcommitAttachRef 更新 ref
  3. 一堆 switch...case... 处理 flags,然后调用 commitPlacement commitWork

下面来看看 commitPlacementcommitWork 做了些啥。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
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);
  // parentStateNode 是否是 rootFiber
  if (isContainer) {
    insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
  } else {
    insertOrAppendPlacementNode(finishedWork, before, parent);
  }
}

commitPlacement 工作流程就是:

  1. getHostParentFiber 获取父级 DOM 节点
  2. getHostSibling 获取兄弟 DOM 节点
  3. insertOrAppendPlacementNodeIntoContainer 或者 insertOrAppendPlacementNode 函数里面会根据 DOM 兄弟节点是否存在决定调用 insert 还是 append 节点
// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
  if (!supportsMutation) {
    // 省略一些代码
  }

  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) {
        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) {
          root.hydrate = false;
          commitHydratedContainer(root.containerInfo);
        }
      }
      return;
    }
    // 省略一些代码
  }
  // 省略一些代码
}

commitWork 函数会根据 tag 的类型,执行不同的代码,比如:

  • FunctionComponent,也就是函数组件:会调用 commitHookEffectListUnmount,执行 useLayoutEffect 这个 hook 的销毁函数
  • HostComponent,也就是原生的 DOM 节点,会调用 commitUpdate 函数,进行 DOM 节点相关属性的更新

layout

// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
  // 省略一些代码
  if (subtreeHasEffects || rootHasEffect) {
    // 省略一些代码

    commitLayoutEffects(finishedWork, root, lanes);

    // 省略一些代码
}

commitLayoutEffects 就是 layout 阶段的入口。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
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;
}

其实这个函数做的事情与前面阶段做的事情也类似,都是先遍历 Fiber树的 child 节点,再遍历其兄弟节点,commitLayoutEffects_begin 调用了 commitLayoutEffects_complete 函数,commitLayoutEffects_complete 又调用了commitLayoutEffectsOnFiber 函数,这个函数代码也比较多。

// 路径:packages/react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutEffectOnFiber(
  finishedRoot: FiberRoot,
  current: Fiber | null,
  finishedWork: Fiber,
  committedLanes: Lanes,
): void {
  if ((finishedWork.flags & LayoutMask) !== NoFlags) {
    switch (finishedWork.tag) {
      case FunctionComponent:
      case ForwardRef:
      case SimpleMemoComponent: {
        if (
          !enableSuspenseLayoutEffectSemantics ||
          !offscreenSubtreeWasHidden
        ) {
          if (
            enableProfilerTimer &&
            enableProfilerCommitHooks &&
            finishedWork.mode & ProfileMode
          ) {
            try {
              startLayoutEffectTimer();
              // 执行 useLayoutEffect 的回调函数
              commitHookEffectListMount(
                HookLayout | HookHasEffect,
                finishedWork,
              );
            } finally {
              recordLayoutEffectDuration(finishedWork);
            }
          } else {
            commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
          }
        }
        break;
      }
      case ClassComponent: {
        const instance = finishedWork.stateNode;
        if (finishedWork.flags & Update) {
          if (!offscreenSubtreeWasHidden) {
            if (current === null) {
              // 省略一些代码
              if (
                enableProfilerTimer &&
                enableProfilerCommitHooks &&
                finishedWork.mode & ProfileMode
              ) {
                try {
                  startLayoutEffectTimer();
                  // componentDidMout 生命周期钩子
                  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();
                  // componentDidUpdate 生命周期钩子
                  instance.componentDidUpdate(
                    prevProps,
                    prevState,
                    instance.__reactInternalSnapshotBeforeUpdate,
                  );
                } finally {
                  recordLayoutEffectDuration(finishedWork);
                }
              } else {
                instance.componentDidUpdate(
                  prevProps,
                  prevState,
                  instance.__reactInternalSnapshotBeforeUpdate,
                );
              }
            }
          }
        }
        // 省略一些代码
      }
      // 省略一些代码
    }
  }

  if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
    if (enableScopeAPI) {
      if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
        // 赋值 ref
        commitAttachRef(finishedWork);
      }
    } else {
      if (finishedWork.flags & Ref) {
        commitAttachRef(finishedWork);
      }
    }
  }
}

commitLayoutEffectsOnFiber 函数主要做的事情:

  1. 调用了函数组件及相关类型的 useLayoutEffect 的回调函数;
  2. 调用 class 组件的 componentDidMountcomponentDidUpdate 生命周期钩子。

总结

  1. 首先,在 before mutation 之前,异步调用了 useEffect
  2. before mutation 阶段,调用 class 组件的 getSnapshotBeforeUpdate生命周期钩子。
  3. mutation 阶段,遇到删除标记,调用 class 组件的 componentWillUnmount 生命周期钩子,从页面移除 Fiber 对应的 DOM 节点;解绑 ref;调度 useEffect 的销毁函数。
  4. mutation 阶段,遇到非删除标记,重置了文本类型节点; 更新 ref;调用 useLayoutEffect 的销毁函数;执行原生的 DOM 节点属性的更新。
  5. layout 阶段,调用 useLayoutEffect 钩子;调用 class 组件的componentDidMountcomponentDidUpdate 生命周期钩子。

由上面的步骤可以看出,useLayoutEffect 是在 DOM 节点更新完成之后同步执行的,因为 useEffect 是异步调用的原因,所以 useLayoutEffect 早于 useEffect 执行。

另外,关注这段代码:

root.current = finishedWork;

它是在 mutation 阶段和 layout 阶段之间执行的,进行 current Fiber树 切换,workInProgress Fiber树 在此变成了 current Fiber树