什么是渲染阶段
渲染的工作由渲染器 (Renderer)来完成,这个阶段被称作 commit 阶段,这个阶段将会把提交的信息渲染到页面上,也就是上一个阶段我们”协调“了半天的 Fiber。
基本流程
从流程上来说 commit 阶段可以分为三个阶段:
- before mutation 阶段,这个阶段 DOM 节点还没有被渲染到界面上去,过程中会触发
getSnapshotBeforeUpdate,也会处理 useEffect 钩子相关的调度逻辑 - mutation 阶段,这个阶段负责 DOM 节点的渲染,在渲染过程中会根据协调阶段的标记(flags),执行不同的 DOM 操作
- layout 阶段,这个阶段处理 DOM 渲染完毕之后的收尾逻辑,比如:
componentDidMount/componentDidUpdate,调用useLayoutEffect钩子函数的回调等,还会把fiberRoot的current指针指向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 函数,这个函数内容比较多,主要分为三个阶段,也就是基本流程中介绍的三个阶段,下面一个一个说。
整体调用过程如下图所示:
在进入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
commitBeforeMutationEffects是 before 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 会执行如下操作:
- 递归调用
Fiber节点及其子孙Fiber节点,fiber.tag为ClassComponent的会调用componentWillUnmount生命周期钩子,从页面移除Fiber对应的 DOM 节点 - 解绑
ref - 调度
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 阶段主要做的事情:
commitResetTextContent最终调用了setTextContent,重置了文本类型节点commitDetachRef、commitAttachRef更新ref- 一堆 switch...case... 处理
flags,然后调用commitPlacement或commitWork
下面来看看 commitPlacement 和 commitWork 做了些啥。
// 路径: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 工作流程就是:
getHostParentFiber获取父级 DOM 节点getHostSibling获取兄弟 DOM 节点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 函数主要做的事情:
- 调用了函数组件及相关类型的
useLayoutEffect的回调函数; - 调用 class 组件的
componentDidMount或componentDidUpdate生命周期钩子。
总结
- 首先,在
before mutation之前,异步调用了useEffect。 - 在
before mutation阶段,调用 class 组件的getSnapshotBeforeUpdate生命周期钩子。 mutation阶段,遇到删除标记,调用 class 组件的componentWillUnmount生命周期钩子,从页面移除Fiber对应的 DOM 节点;解绑ref;调度useEffect的销毁函数。mutation阶段,遇到非删除标记,重置了文本类型节点; 更新ref;调用useLayoutEffect的销毁函数;执行原生的 DOM 节点属性的更新。layout阶段,调用useLayoutEffect钩子;调用 class 组件的componentDidMount或componentDidUpdate生命周期钩子。
由上面的步骤可以看出,useLayoutEffect 是在 DOM 节点更新完成之后同步执行的,因为 useEffect 是异步调用的原因,所以 useLayoutEffect 早于 useEffect 执行。
另外,关注这段代码:
root.current = finishedWork;
它是在 mutation 阶段和 layout 阶段之间执行的,进行 current Fiber树 切换,workInProgress Fiber树 在此变成了 current Fiber树。