「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」。
react 版本:v17.0.3
在《React源码解读之任务调度流程》一文中我们将 reconciler 的过程分为了四个节点,其中第四个阶段是输出DOM节点阶段,也即是 commit 阶段,它负责与渲染器(react-dom) 交互,渲染DOM节点。
commit 阶段主要做的事情,是根据之前生成的 effectList,对相应的真实DOM进行更新和渲染,这个阶段是不可中断。
commitRoot
commitRoot 函数是commit阶段的入口函数,我们来看看它的源码:
// react-reconciler/src/ReactFiberWorkLoop.new.js
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;
// 将更新优先级置为 DiscreteEventPriority (事件最高优先级)
setCurrentUpdatePriority(DiscreteEventPriority);
// 执行提交
commitRootImpl(root, previousUpdateLanePriority);
} finally {
// 重置 transition 和 更新优先级
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
可以看到,commitRoot 只是将更新的优先级设置为了事件最高优先级,然后调用commitRootImpl函数来处理副作用,将最新的fiber树结构反映到DOM上。
接下来我们看看 commitRootImpl 函数。
commitRootImpl
// packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
// do while 循环,执行所有的副作用
do {
// flushPassiveEffects 在最后会调用 flushSyncUpdateQueue
// 循环执行 flushPassiveEffects,直到没有挂载阶段的副作用
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// ...
if (subtreeHasEffects || rootHasEffect) {
// 有副作用,处理 fiber树上的副作用
// ...
// 第一个阶段是 before mutation ,在这个阶段可以读取改变之前的 host tree 的state
// 这个阶段是 生命周期函数getSnapshotBeforeUpdate 调用的地方
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
// ...
// The next phase is the mutation phase, where we mutate the host tree.
// 下一个阶段是 mutation phase,可以在这个阶段 改变 host tree
commitMutationEffects(root, finishedWork, lanes);
// ...
// 下一个阶段是 layout phase(布局阶段)
// 提交 layout 阶段的副作用
commitLayoutEffects(finishedWork, root, lanes);
// ...
// Tell Scheduler to yield at the end of the frame, so the browser has an
// opportunity to paint.
// 告诉调度器在帧结束时让出,这样浏览器就有机会进行绘制。
requestPaint();
// 重置执行栈环境
executionContext = prevExecutionContext;
// Reset the priority to the previous non-sync value.
// 将优先级重置为之前的 非同步优先级
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// 没有副作用
// ...
}
// ...
// 在退出 commitRoot 之前总是调用 ensureRootIsScheduled(),确保 root 节点上的任何额外任务都被调度
ensureRootIsScheduled(root, now());
// ...
// If layout work was scheduled, flush it now.
// 布局工作已安排,立即刷新它
flushSyncCallbacks();
// ...
}
commitRootImpl 的核心处理逻辑可分为三个部分,也就是commit阶段的三个阶段:
-
before mutation 阶段,这个阶段通过执行 commitBeforeMutationEffects 函数来更新 class 组件实例上的 state、props 等,并且这个阶段是生命周期函数 getSnapshotBeforeUpdate 调用的地方。
-
mutation phase 阶段(挂载阶段),这个阶段通过调用 commitMutationEffects 来完成副作用的执行,主要是处理副作用队列中带有Placement、Update、Deletion、Hydrating标记的fiber节点,与 react-dom 交互,完成DOM节点的插入、更新以及删除操作。
-
layout phase 阶段(布局阶段),这个阶段通过执行 commitLayoutEffects 函数来处理副作用队列中带有Update | Callback标记的fiber节点,并触发 componentDidMount、componentDidUpdate 以及各种回调函数等。
before mutation 阶段
在 before mutation 阶段,我们需要重点关注的是 commitBeforeMutationEffectsOnFiber 函数,下面,我们从before mutation 阶段的入口函数 commitBeforeMutationEffects 看起。
commitBeforeMutationEffects
// react-reconciler/src/ReactFiberCommitWork.new.js
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
// 调用 ReactDOM的 getClosestInstanceFromNode 方法
// 获取当前节点 最近的 HostComponent 或 HostText fiber祖先节点
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
// 创建 beforeblur 事件并派发
commitBeforeMutationEffects_begin();
// We no longer need to track the active instance fiber
// 不再跟踪fiber节点
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
commitBeforeMutationEffects 函数的逻辑比较简单,其主要做的事情就是初始化全局变量nextEffect (nextEffect变量在commit的整个阶段都会使用到),然后调用 commitBeforeMutationEffects_begin 函数来处理副作用。
commitBeforeMutationEffects_begin
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// This phase is only used for beforeActiveInstanceBlur.
// Let's skip the whole loop if it's off.
if (enableCreateEventHandleAPI) {
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const deletion = deletions[i];
// 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
commitBeforeMutationEffectsDeletion(deletion);
}
}
}
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
ensureCorrectReturnPointer(child, fiber);
nextEffect = child;
} else {
// 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
// 更新fiber节点的 props 和 state
commitBeforeMutationEffects_complete();
}
}
}
commitBeforeMutationEffects_begin函数做的事情,就是遍历fiber树,对每个fiber节点,调用commitBeforeMutationEffects_complete 函数来更新fiber节点的 props 和 state 。
commitBeforeMutationEffects_complete
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
// 在 ReactDOM 中调用 dispatchBeforeDetachedBlur()创建 beforeblur 事件并派发
// 更新fiber节点的 props 和 state
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;
}
}
在 commitBeforeMutationEffects_complete 函数中,继续对fiber树进行遍历,然后调用 commitBeforeMutationEffectsOnFiber 函数来更新fiber节点的 props 和 state 。
下面,我们重点来看下 before mutation 阶段的 commitBeforeMutationEffectsOnFiber 函数。
commitBeforeMutationEffectsOnFiber
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// ...
if ((flags & Snapshot) !== NoFlags) {
// ...
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
break;
}
case ClassComponent: {
if (current !== null) {
// 非首次渲染的情况
// 获取上一次的props
const prevProps = current.memoizedProps;
// 获取上一次的 state
const prevState = current.memoizedState;
// 获取当前 class组件实例
const instance = finishedWork.stateNode;
// 更新 props 和 state
// ...
// 调用 getSnapshotBeforeUpdate 生命周期方法
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
// ...
// 将生成的 snapshot 保存到 instance.__reactInternalSnapshotBeforeUpdate 上
// 供 DidUpdate 生命周期使用
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
// ...
}
// ...
}
}
可以看到,在 commitBeforeMutationEffectsOnFiber 函数中,根据fiber节点的tag属性,主要是对 ClassComponent 进行处理,更新 ClassComponent 实例上的 state、props 等,并执行 getSnapshotBeforeUpdate 生命周期函数。
mutation phase 阶段(挂载阶段)
在 mutation phase 阶段,通过遍历整棵fiber树,对fiber节点执行插入、更新及删除操作。
commitMutationEffects
// react-reconciler/src/ReactFiberCommitWork.new.js
export function commitMutationEffects(
root: FiberRoot,
firstChild: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = firstChild;
commitMutationEffects_begin(root);
inProgressLanes = null;
inProgressRoot = null;
}
commitMutationEffects 同样对全局变量 nextEffect 进行了赋值,然后调用 commitMutationEffects_begin 函数,传入 root 节点,执行副作用。
commitMutationEffects_begin
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffects_begin(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
// TODO: Should wrap this in flags check, too, as optimization
const deletions = fiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 断开当前fiber节点与父节点之间的连接
// 分离 refs 引用并在整棵子树上调用 componentWillUnmount周期函数
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);
}
}
}
在 commitMutationEffects_begin 中,也是对fiber树进行遍历,如果fiber上有 Deletion副作用标记,则调用commitDeletion 分离 refs 引用,并在子树上调用 componentWillUnmount 生命周期函数,断开当前fiber节点与父节点之间的连接。最后调用 commitMutationEffects_complete 来执行 mutation phase 阶段的副作用。
commitMutationEffects_complete
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffects_complete(root: FiberRoot) {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
// 根据不同的组件类型,提交work
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;
}
}
在 commitMutationEffects_complete 函数中,继续对fiber树进行遍历,然后调用 commitMutationEffectsOnFiber 函数,根据不同的组件类型,来执行更新、插入、删除操作。
下面,我们重点来看下 mutation phase 阶段的 commitMutationEffectsOnFiber 函数。
commitMutationEffectsOnFiber
由于在 commitMutationEffects_begin 函数和 commitMutationEffects_complete 函数中已经对fiber树进行了遍历,并且已执行了fiber的删除操作,因此 commitMutationEffectsOnFiber 函数就是对单个fiber节点执行更新、插入操作。
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitMutationEffectsOnFiber(finishedWork: Fiber, root: FiberRoot) {
// TODO: The factoring of this phase could probably be improved. Consider
// switching on the type of work before checking the flags. That's what
// we do in all the other phases. I think this one is only different
// because of the shared reconciliation logic below.
const flags = finishedWork.flags;
// 重置 文本节点
if (flags & ContentReset) {
commitResetTextContent(finishedWork);
}
// 更新 Ref
if (flags & Ref) {
const current = finishedWork.alternate;
if (current !== null) {
// 更新 ref 的current 值
commitDetachRef(current);
}
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (finishedWork.tag === ScopeComponent) {
commitAttachRef(finishedWork);
}
}
}
// ...
// 执行更新、插入操作
const primaryFlags = flags & (Placement | Update | Hydrating);
outer: switch (primaryFlags) {
// 插入
case Placement: {
commitPlacement(finishedWork);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
// TODO: findDOMNode doesn't rely on this any more but isMounted does
// and isMounted is deprecated anyway so we should be able to kill this.
finishedWork.flags &= ~Placement;
break;
}
// 插入并更新
case PlacementAndUpdate: {
// 插入
// Placement
commitPlacement(finishedWork);
// Clear the "placement" from effect tag so that we know that this is
// inserted, before any life-cycles like componentDidMount gets called.
finishedWork.flags &= ~Placement;
// 更新
// Update
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
case Hydrating: {
finishedWork.flags &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
finishedWork.flags &= ~Hydrating;
// Update
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
// 更新
case Update: {
const current = finishedWork.alternate;
commitWork(current, finishedWork);
break;
}
}
}
在 commitMutationEffectsOnFiber 中,会根据 fiber上的 flags 的类型进行 二进制与 运算,然后根据运算后的结果去执行不同的操作,对真实DOM进行修改:
-
ContentReset:如果 flags 中包含 ContentReset 类型,说明文本节点内容发生改变,则执行 commitResetTextContent 函数重置文本节点的内容。
-
Ref:如果 flags 中包含 Ref 类型,则执行 commitDetachRef 函数更改 ref 对应的 current 的值。
-
Placement:如果 flags 中包含 Placement 类型,代表需要插入新节点,执行 commitPlacement 函数插入DOM节点。
-
Update:如果 flags 中包含 Update 类型,则执行 commitWork 执行更新操作。
-
PlacementAndUpdate:如果是 PlacementAndUpdate 类型,则先调用 commitPlacement 执行插入操作,然后再调用 commitWork 执行更新操作。
下面,我们再来看看react是如何对真实DOM节点进行插入、更新及删除操作的。
插入DOM节点
无论 fiber 的 flags 中包含 Placement 还是 PlacementAndUpdate,都是调用 commitPlacement 执行DOM节点插入操作。
commitPlacement -- 获取父节点及插入位置
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
// Recursively insert all host nodes into the parent.
// 获取当前fiber节点的父fiber节点
const parentFiber = getHostParentFiber(finishedWork);
// Note: these two variables *must* always be updated together.
// 这两个变量总是会一起被更新
let parent;
let isContainer;
// 获取父fiber节点对应的真实DOM节点
const parentStateNode = parentFiber.stateNode;
// 根据父fiber节点的 tag 的类型(标签类型) 来获取 父fiber节点对应的DOM节点是否可以作为 container
switch (parentFiber.tag) {
// tag类型为 HostComponent,不可以作为 container
case HostComponent:
parent = parentStateNode;
isContainer = false;
break;
// tag 类型为 HostRoot,可以作为 container
case HostRoot:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
// tag 类型为 HostPortal,可以作为 container
case HostPortal:
parent = parentStateNode.containerInfo;
isContainer = true;
break;
// eslint-disable-next-line-no-fallthrough
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
// 如果父 fiber节点有 ContentReset 的 flags 副作用,则重置其文本内容
if (parentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
parentFiber.flags &= ~ContentReset;
}
// 获取要在哪个兄弟fiber节点之前插入
const before = getHostSibling(finishedWork);
// We only have the top Fiber that was inserted but we need to recurse down its
// children to find all the terminal nodes.
if (isContainer) {
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
} else {
insertOrAppendPlacementNode(finishedWork, before, parent);
}
}
在 commitPlacement 中,首先获取当前fiber节点的父fiber节点对应的真实DOM节点以及在父节点下要插入的位置,然后根据fiber节点的 tag 类型 (标签类型) 来判断父fiber节点对应的DOM节点是否可以作为 container 容器,如果可以作为container,则调用 insertOrAppendPlacementNodeIntoContainer 函数在指定的位置插入节点,否则调用 insertOrAppendPlacementNode 函数在指定的位置插入新节点。
insertOrAppendPlacementNodeIntoContainer -- 判断是否为单节点
insertOrAppendPlacementNodeIntoContainer 和 insertOrAppendPlacementNode 的处理逻辑是一致,唯一的区别就是 insertOrAppendPlacementNode 在对应的位置插入节点时,不需要额外判断父节点(container) 的节点类型(nodeType) 是否为注释类型节点(COMMENT_NODE) 。因此我们以 insertOrAppendPlacementNodeIntoContainer 为例,看看react如何在对应的位置插入DOM节点的。
// react-reconciler/src/ReactFiberCommitWork.new.js
function insertOrAppendPlacementNodeIntoContainer(
node: Fiber,
before: ?Instance,
parent: Container,
): void {
const { tag } = node;
// 判断当前节点是否为原生的 DOM节点
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
// 是原生DOM节点
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.
// 是Portal 则不做处理
} else {
// 不是原生DOM节点,则遍历插入当前节点的各个子节点
const child = node.child;
// 继续调用 insertOrAppendPlacementNodeIntoContainer
// 直到找到原生DOM节点,然后插入节点
if (child !== null) {
insertOrAppendPlacementNodeIntoContainer(child, before, parent);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNodeIntoContainer(sibling, before, parent);
sibling = sibling.sibling;
}
}
}
}
在 insertOrAppendPlacementNodeIntoContainer 中,通过 fiber 的 tag 属性来判断当前fiber节点是否为原生DOM节点(HostComponent 或 HostText)。
如果是,则调用 insertInContainerBefore 或 appendChildToContainer 在相应的位置插入DOM节点。
如果不是原生DOM节点,则对当前fiber节点的所有子fiber节点调用 insertOrAppendPlacementNodeIntoContainer 自身进行遍历,直到找到原生DOM节点,然后插入节点。
insertInContainerBefore -- 在对应位置插入节点
// react-dom/src/client/ReactDOMHostConfig.js
export function insertInContainerBefore(
container: Container,
child: Instance | TextInstance,
beforeChild: Instance | TextInstance | SuspenseInstance,
): void {
if (container.nodeType === COMMENT_NODE) {
// 如果父节点为注释类型,则在父节点的父节点下插入新的DOM
// 调用原生DOM的 insertBefore 方法插入节点
(container.parentNode: any).insertBefore(child, beforeChild);
} else {
// 父节点不是注释类型,则直接插入新的DOM
// 调用原生DOM的 insertBefore 方法插入节点
container.insertBefore(child, beforeChild);
}
}
在 insertOrAppendPlacementNodeIntoContainer 函数中,如果 before 不为null,说明要在某个DOM节点之前插入新的DOM节点,此时调用 insertInContainerBefore 函数去进行插入,根据父节点是否为注释类型(nodeType 为 COMMENT_NODE) ,选择是在父节点的父节点下插入新的DOM节点还是直接在父节点下插入新的DOM节点,插入节点时调用的是原生DOM元素的 insertBefore 方法。
appendChildToContainer -- 在末尾追加节点
// react-dom/src/client/ReactDOMHostConfig.js
export function appendChildToContainer(
container: Container,
child: Instance | TextInstance,
): void {
let parentNode;
if (container.nodeType === COMMENT_NODE) {
// 如果父节点是注释类型,则在父节点的父节点下插入新的DOM节点
// 调用原生DOM的 insertBefore 方法插入新的节点
parentNode = (container.parentNode: any);
parentNode.insertBefore(child, container);
} else {
// 父节点不是注释类型,则直接插入新的DOM节点
parentNode = container;
// 调用原生DOM的appendChild 方法在末尾添加新的子节点
parentNode.appendChild(child);
}
// This container might be used for a portal.
// If something inside a portal is clicked, that click should bubble
// through the React tree. However, on Mobile Safari the click would
// never bubble through the *DOM* tree unless an ancestor with onclick
// event exists. So we wouldn't see it and dispatch it.
// This is why we ensure that non React root containers have inline onclick
// defined.
// https://github.com/facebook/react/issues/11918
const reactRootContainer = container._reactRootContainer;
if (
(reactRootContainer === null || reactRootContainer === undefined) &&
parentNode.onclick === null
) {
// TODO: This cast may not be sound for SVG, MathML or custom elements.
trapClickOnNonInteractiveElement(((parentNode: any): HTMLElement));
}
}
在 insertOrAppendPlacementNodeIntoContainer 函数中,如果 before 为 null,则调用 appendChildToContainer 函数进行插入,在插入前,会判断父节点是否为注释类型,如果是,则调用原生DOM元素的 insertBefore 方法插入新节点;如果不是,则调用原生DOM元素的 appendChild 方法插入新节点。无论是调用 insertBefore 方法还是 appendChild 方法,appendChildToContainer 插入新节点都是在末尾的位置追加新的DOM节点。
可以看到,insertInContainerBefore 和 appendChildToContainer 的区别是:insertInContainerBefore 是在指定的位置插入新的DOM节点,而 appendChildToContainer是在末尾追加新的DOM节点。
更新DOM节点
当 fiber 的 flags 中包含 Update 还是 PlacementAndUpdate 时,都是调用 commitWork 执行DOM节点的更新操作。
commitWork
在 commitWork 中,主要是针对 HostComponent 和 HostText 两种类型的更新。
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitWork(current: Fiber | null, finishedWork: Fiber): void {
// ...
switch (finishedWork.tag) {
// ...
case ClassComponent: {
return;
}
case HostComponent: {
// 获取真实DOM节点
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
// 获取新的 props
const newProps = finishedWork.memoizedProps;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
// 获取旧的props
const oldProps = current !== null ? current.memoizedProps : newProps;
const type = finishedWork.type;
// TODO: Type the updateQueue to be specific to host components.
// 取出 updateQueue
const updatePayload: null | UpdatePayload = (finishedWork.updateQueue: any);
// 清空 fiber 上的 updateQueue
finishedWork.updateQueue = null;
if (updatePayload !== null) {
// 提交更新
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
}
}
return;
}
case HostText: {
if (finishedWork.stateNode === null) {
throw new Error(
'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;
// For hydration we reuse the update path but we treat the oldProps
// as the newProps. The updatePayload will contain the real change in
// this case.
// 获取旧的文本内容
const oldText: string =
current !== null ? current.memoizedProps : newText;
// 提交更新
commitTextUpdate(textInstance, oldText, newText);
return;
}
case HostRoot: {
if (supportsHydration) {
const root: FiberRoot = finishedWork.stateNode;
if (root.isDehydrated) {
// We've just hydrated. No need to hydrate again.
root.isDehydrated = false;
commitHydratedContainer(root.containerInfo);
}
}
return;
}
case Profiler: {
return;
}
// ...
}
// ...
}
对于 HostComponent 类型的更新,首先获取真实的DOM节点,节点的props 以及 updateQueue,然后调用 commitUpdate 对DOM进行更新。
对于 HostText 类型的更新,也是首先获取真实的文本节点,新旧文本内容,然后调用commitTextUpdate更新文本内容。
commitUpdate -- 更新HostComponent
// react-dom/src/client/ReactDOMHostConfig.js
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// Update the props handle so that we know which props are the ones with
// with current event handlers.
// 做了 domElement[internalPropsKey] = props 的操作
updateFiberProps(domElement, newProps);
// Apply the diff to the DOM node.
// 将 diff 结果应用于真实DOM
updateProperties(domElement, updatePayload, type, oldProps, newProps);
}
在 commitUpdate 函数中,首先调用 updateFiberProps 执行domElement[internalPropsKey] = props 的操作,然后调用 updateProperties 函数将 diff 结果应用于真实DOM上。
updateProperties
// react-dom/src/client/ReactDOMComponent.js
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// Update checked *before* name.
// In the middle of an update, it is possible to have multiple checked.
// When a checked radio tries to change name, browser makes another radio's checked false.
// 根据表单类型进行特殊的处理,例如更新 radio 的checked 值
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}
// 判断是否为用户自定义的组件,即是否包含 “-”
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// Apply the diff.
// 将 diff 结果应用于真实DOM
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// TODO: Ensure that an update gets scheduled if any of the special props
// changed.
// 针对表单的特殊处理
switch (tag) {
case 'input':
// Update the wrapper around inputs *after* updating props. This has to
// happen after `updateDOMProperties`. Otherwise HTML5 input validations
// raise warnings and prevent the new value from being assigned.
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
// <select> value update needs to occur after <option> children
// reconciliation
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
在 updateProperties 中,首先对 radio 类型的表单DOM节点进行特殊的处理,然后调用 updateDOMProperties 函数,将 diff 结果应用于真实的DOM节点。最后根据 fiber 的 tag 类型,对 input、textarea、select 等表单类型的DOM节点做特殊处理。
updateDOMProperties
// react-dom/src/client/ReactDOMComponent.js
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// TODO: Handle wasCustomComponentTag
// 遍历 updatePayload,即遍历 updateQueue
for (let i = 0; i < updatePayload.length; i += 2) {
const propKey = updatePayload[i];
const propValue = updatePayload[i + 1];
if (propKey === STYLE) {
// 处理 style 样式更新
setValueForStyles(domElement, propValue);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 处理 innerHTML 改变
setInnerHTML(domElement, propValue);
} else if (propKey === CHILDREN) {
// 处理 textContent
setTextContent(domElement, propValue);
} else {
// 处理其它节点属性
setValueForProperty(domElement, propKey, propValue, isCustomComponentTag);
}
}
}
在 updateDOMProperties 中,会遍历在render阶段生成的 updatePayload,即 updateQueue,将其映射到真实DOM节点的属性上。根据 propKey 的类型,对 style、innerHTML以及 textContext做处理,从而实现DOM的更新。
commitTextUpdate -- 更新 HostText
// react-dom/src/client/ReactDOMHostConfig.js
export function commitTextUpdate(
textInstance: TextInstance,
oldText: string,
newText: string,
): void {
textInstance.nodeValue = newText;
}
HostText 的更新处理则非常简单,直接将DOM节点的nodeValue属性重新赋值为 newText 的值即可。
删除DOM节点
mutation phase 阶段,在 commitMutationEffects_begin 函数中,对fiber树进行遍历时,如果fiber上有 Deletion副作用标记,则调用commitDeletion 来执行删除操作。
commitDeletion
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitDeletion(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
if (supportsMutation) {
// 支持 useMutation
// Recursively delete all host nodes from the parent.
// Detach refs and call componentWillUnmount() on the whole subtree.
// 通过深度优先遍历,从父节点开始递归删除所有的 host节点
// 卸载 refs 引用并在整棵子树上调用 componentWillUnmount 生命周期函数
unmountHostComponents(finishedRoot, current, nearestMountedAncestor);
} else {
// 不支持 useMutation
// Detach refs and call componentWillUnmount() on the whole subtree.
// 通过深度优先遍历,卸载 refs 引用并在整棵子树上调用 componentWillUnmount 生命周期函数
commitNestedUnmounts(finishedRoot, current, nearestMountedAncestor);
}
// 重置fiber的各项属性,将其重置为 null,断开当前fiber节点与父节点之间的连接
detachFiberMutation(current);
}
commitDeletion 函数是删除DOM节点操作的入口函数,在commitDeletion函数中,会调用 unmountHostComponents 和 commitNestedUnmounts,通过深度优先遍历整棵fiber树,找到需要删除的fiber节点,卸载 ref 引用,执行 componentWillUnmount 生命周期函数,将对应的DOM节点删除。最后调用 detachFiberMutation 函数,将当前fiber节点上的各项属性重置为 null,断开当前fiber节点与父节点之间的连接。
unmountHostComponents
// react-reconciler/src/ReactFiberCommitWork.new.js
function unmountHostComponents(
finishedRoot: FiberRoot,
current: Fiber,
nearestMountedAncestor: Fiber,
): void {
// We only have the top Fiber that was deleted but we need to recurse down its
// children to find all the terminal nodes.
let node: Fiber = current;
// Each iteration, currentParent is populated with node's host parent if not
// currentParentIsValid.
let currentParentIsValid = false;
// Note: these two variables *must* always be updated together.
let currentParent;
let currentParentIsContainer;
while (true) {
if (!currentParentIsValid) {
// 如果当前的父节点不是非法的DOM节点,则寻找一个合法的DOM父节点
let parent = node.return;
findParent: while (true) {
if (parent === null) {
throw new Error(
'Expected to find a host parent. This error is likely caused by ' +
'a bug in React. Please file an issue.',
);
}
const parentStateNode = parent.stateNode;
switch (parent.tag) {
// HostComponent 不可以作为container
case HostComponent:
currentParent = parentStateNode;
currentParentIsContainer = false;
break findParent;
// HostRoot 可以作为container
case HostRoot:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
// HostPortal 可以作为container
case HostPortal:
currentParent = parentStateNode.containerInfo;
currentParentIsContainer = true;
break findParent;
}
parent = parent.return;
}
currentParentIsValid = true;
}
if (node.tag === HostComponent || node.tag === HostText) {
// 如果是原生DOM节点或文本节点,调用 commitNestedUnmounts 卸载DOM节点
commitNestedUnmounts(finishedRoot, node, nearestMountedAncestor);
// After all the children have unmounted, it is now safe to remove the
// node from the tree.
if (currentParentIsContainer) {
// 移除当前container下的子节点
removeChildFromContainer(
((currentParent: any): Container),
(node.stateNode: Instance | TextInstance),
);
} else {
// 移除子节点
removeChild(
((currentParent: any): Instance),
(node.stateNode: Instance | TextInstance),
);
}
// Don't visit children because we already visited them.
} else if (
enableSuspenseServerRenderer &&
node.tag === DehydratedFragment
) {
// Fragment 节点
if (enableSuspenseCallback) {
const hydrationCallbacks = finishedRoot.hydrationCallbacks;
if (hydrationCallbacks !== null) {
const onDeleted = hydrationCallbacks.onDeleted;
if (onDeleted) {
onDeleted((node.stateNode: SuspenseInstance));
}
}
}
// Delete the dehydrated suspense boundary and all of its content.
if (currentParentIsContainer) {
clearSuspenseBoundaryFromContainer(
((currentParent: any): Container),
(node.stateNode: SuspenseInstance),
);
} else {
clearSuspenseBoundary(
((currentParent: any): Instance),
(node.stateNode: SuspenseInstance),
);
}
} else if (node.tag === HostPortal) {
// Portal 节点,直接向下遍历 child,因为它没有 ref 和生命周期等额外要处理的事情
if (node.child !== null) {
// When we go into a portal, it becomes the parent to remove from.
// We will reassign it back when we pop the portal on the way up.
currentParent = node.stateNode.containerInfo;
currentParentIsContainer = true;
// Visit children because portals might contain host components.
node.child.return = node;
node = node.child;
continue;
}
} else {
// 其它 react 节点,调用 commitUnmount,卸载 ref、执行生命周期函数等
commitUnmount(finishedRoot, node, nearestMountedAncestor);
// Visit children because we may find more host components below.
// 深度优先遍历子节点
if (node.child !== null) {
node.child.return = node;
node = node.child;
continue;
}
}
// node 和 current 相等时,说明整棵树的深度遍历完成
if (node === current) {
return;
}
// 如果当前遍历到的子节点没有兄弟节点,说明当前子树遍历完毕,返回到父节点继续深度遍历
while (node.sibling === null) {
if (node.return === null || node.return === current) {
return;
}
node = node.return;
if (node.tag === HostPortal) {
// When we go out of the portal, we need to restore the parent.
// Since we don't keep a stack of them, we will search for it.
currentParentIsValid = false;
}
}
// 继续遍历兄弟节点
node.sibling.return = node.return;
node = node.sibling;
}
}
在 unmountHostComponents 函数中,会首先判断当前的父节点是否是合法的DOM节点,如果不合法,则寻找一个合法的父节点。然后通过深度优先遍历,去遍历整棵fiber树,如果遍历的的节点是HostComponent或HostText ,则调用commitNestedUnmounts函数卸载ref引用,执行componentWillUnmount生命周期函数,如果是其它的react节点,则调用 commitUnmount 函数卸载 ref引用,执行 componentWillUnmount 生命周期函数。
commitNestedUnmounts
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitNestedUnmounts(
finishedRoot: FiberRoot,
root: Fiber,
nearestMountedAncestor: Fiber,
): void {
// While we're inside a removed host node we don't want to call
// removeChild on the inner nodes because they're removed by the top
// call anyway. We also want to call componentWillUnmount on all
// composites before this host node is removed from the tree. Therefore
// we do an inner loop while we're still inside the host node.
let node: Fiber = root;
while (true) {
// 调用 commitUnmount 卸载 ref ,执行生命周期函数
commitUnmount(finishedRoot, node, nearestMountedAncestor);
// Visit children because they may contain more composite or host nodes.
// Skip portals because commitUnmount() currently visits them recursively.
if (
node.child !== null &&
// If we use mutation we drill down into portals using commitUnmount above.
// If we don't use mutation we drill down into portals here instead.
(!supportsMutation || node.tag !== HostPortal)
) {
// 深度优先遍历向下遍历子树
node.child.return = node;
node = node.child;
continue;
}
// node 与 root 相等时说明整棵树的深度优先遍历已完成
if (node === root) {
return;
}
// 当前子节点没有兄弟节点,说明当前子树已经遍历完成,返回父节点继续深度遍历
while (node.sibling === null) {
if (node.return === null || node.return === root) {
return;
}
node = node.return;
}
// 遍历兄弟节点
node.sibling.return = node.return;
node = node.sibling;
}
}
commitNestedUnmounts 函数同样是通过深度优先遍历,去遍历整棵fiber树,然后执行commitUnmount方法卸载 ref引用,执行componentWillUnmount生命周期函数。它与unmountHostComponents的不同点就是,commitNestedUnmounts不需要判断当前父节点是否是合法的DOM节点以及react节点类型的判断,而是直接调用commitUnmount函数执行删除操作。
commitUnmount
// react-reconciler/src/ReactFiberCommitWork.new.js
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) {
// 获取effect 链上的第一个副作用
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const { destroy, tag } = effect;
// 销毁副作用
if (destroy !== undefined) {
if (
(tag & HookInsertion) !== NoHookEffect ||
(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: {
// 卸载 ref 引用
safelyDetachRef(current, nearestMountedAncestor);
// 获取当前组件实例
const instance = current.stateNode;
// 执行 componentWillUnmount 生命周期函数
if (typeof instance.componentWillUnmount === 'function') {
safelyCallComponentWillUnmount(
current,
nearestMountedAncestor,
instance,
);
}
return;
}
case HostComponent: {
// 卸载 ref 引用
safelyDetachRef(current, nearestMountedAncestor);
return;
}
case HostPortal: {
// TODO: this is recursive.
// We are also not using this parent because
// the portal will get pushed immediately.
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;
}
}
}
在 commitUnmount 函数中,会根据组件的类型,去销毁fiber上的副作用,卸载ref引用,如果是ClassComponent,还会执行 componentWillUnmount 生命周期函数。
通过以上的这些操作,react最终完成了DOM的删除操作。
layout phase 阶段(布局阶段)
commitLayoutEffects
// 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;
}
和 before mutation 阶段和 mutation phase 阶段一样,layout phase 节点的入口函数commitLayoutEffects首先对全局变量nextEffect进行了赋值,然后调用 commitLayoutEffects_begin 函数来处理副作用,并触发 componentDidMount、componentDidUpdate 以及各种回调函数等。
commitLayoutEffects_begin
// react-reconciler/src/ReactFiberCommitWork.new.js
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 (
enableSuspenseLayoutEffectSemantics &&
fiber.tag === OffscreenComponent &&
isModernRoot
) {
// Keep track of the current Offscreen stack's state.
// 跟踪当前屏幕外堆栈的状态
const isHidden = fiber.memoizedState !== null;
const newOffscreenSubtreeIsHidden = isHidden || offscreenSubtreeIsHidden;
if (newOffscreenSubtreeIsHidden) {
// The Offscreen tree is hidden. Skip over its layout effects.
// 屏幕外树被隐藏。 跳过其布局效果。
//遍历 alternate 树进行布局,循环处理兄弟节点和父节点
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
continue;
} else {
// TODO (Offscreen) Also check: subtreeFlags & LayoutMask
const current = fiber.alternate;
const wasHidden = current !== null && current.memoizedState !== null;
const newOffscreenSubtreeWasHidden =
wasHidden || offscreenSubtreeWasHidden;
const prevOffscreenSubtreeIsHidden = offscreenSubtreeIsHidden;
const prevOffscreenSubtreeWasHidden = offscreenSubtreeWasHidden;
// Traverse the Offscreen subtree with the current Offscreen as the root.
// 以当前屏幕外的fiber树(alternate) 为根,遍历 alternate 的子树
offscreenSubtreeIsHidden = newOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = newOffscreenSubtreeWasHidden;
if (offscreenSubtreeWasHidden && !prevOffscreenSubtreeWasHidden) {
// This is the root of a reappearing boundary. Turn its layout effects
// back on.
// 重新出现的根,重新打开其布局效果
nextEffect = fiber;
reappearLayoutEffects_begin(fiber);
}
let child = firstChild;
while (child !== null) {
nextEffect = child;
// 处理下一个节点的布局,递归调用 commitLayoutEffects_begin 自身
commitLayoutEffects_begin(
child, // New root; bubble back up to here and stop.
root,
committedLanes,
);
child = child.sibling;
}
// Restore Offscreen state and resume in our-progress traversal.
nextEffect = fiber;
offscreenSubtreeIsHidden = prevOffscreenSubtreeIsHidden;
offscreenSubtreeWasHidden = prevOffscreenSubtreeWasHidden;
//遍历 alternate 树进行布局,循环处理兄弟节点和父节点
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
continue;
}
}
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
ensureCorrectReturnPointer(firstChild, fiber);
nextEffect = firstChild;
} else {
//遍历 alternate 树进行布局,循环处理兄弟节点和父节点
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
在 commitLayoutEffects_begin 函数中,从nextEffect开始,向下遍历子树,调用 commitLayoutMountEffects_complete 函数来处理副作用,触发 componentDidMount、componentDidUpdate 以及各种回调函数等。
commitLayoutMountEffects_complete
// react-reconciler/src/ReactFiberCommitWork.new.js
function commitLayoutMountEffects_complete(
subtreeRoot: Fiber,
root: FiberRoot,
committedLanes: Lanes,
) {
//遍历 alternate 树进行布局,循环处理兄弟节点和父节点
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
setCurrentDebugFiberInDEV(fiber);
try {
// 根据不同的组件类型,实现布局效果
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
reportUncaughtErrorInDEV(error);
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
}
// fiber 树已遍历完
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
// 遍历兄弟节点
const sibling = fiber.sibling;
if (sibling !== null) {
ensureCorrectReturnPointer(sibling, fiber.return);
nextEffect = sibling;
return;
}
// 回到父节点,继续遍历其它节点
nextEffect = fiber.return;
}
}
在 commitLayoutMountEffects_complete 函数中,继续对 nextEffect 进行遍历,从 nextEffect 开始,遍历整棵树,继续调用 commitLayoutEffectOnFiber 函数,根据不同的组件类型,处理相关的副作用以及执行对应的生命周期函数。
commitLayoutEffectOnFiber -- 执行生命周期
// 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
) {
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 通过 do...while 循环 挂载副作用
commitHookEffectListMount(
HookLayout | HookHasEffect,
finishedWork,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
// 通过 do...while 循环 挂载副作用
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}
}
break;
}
case ClassComponent: {
const instance = finishedWork.stateNode;
if (finishedWork.flags & Update) {
if (!offscreenSubtreeWasHidden) {
if (current === null) {
// 首次渲染
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
// ...
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 执行 componentDidMount 生命周期函数
instance.componentDidMount();
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
// 执行 componentDidMount 生命周期函数
instance.componentDidMount();
}
} else {
// 非首次渲染
// 获取旧的 props
const prevProps =
finishedWork.elementType === finishedWork.type
? current.memoizedProps
: resolveDefaultProps(
finishedWork.type,
current.memoizedProps,
);
// 获取旧的 state
const prevState = current.memoizedState;
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
// ...
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 执行 componentDidUpdate 生命周期函数
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
} finally {
recordLayoutEffectDuration(finishedWork);
}
} else {
// 执行 componentDidUpdate 生命周期函数
instance.componentDidUpdate(
prevProps,
prevState,
instance.__reactInternalSnapshotBeforeUpdate,
);
}
}
}
}
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
// ...
// We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
// 在 commitUpdateQueue 函数中,遍历updateQueue,执行effect副作用
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostRoot: {
// TODO: I think this is now always non-null by the time it reaches the
// commit phase. Consider removing the type check.
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;
}
}
// 在 commitUpdateQueue 函数中,遍历updateQueue,执行effect副作用
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
// Renderers may schedule work to be done after host components are mounted
// (eg DOM renderer may schedule auto-focus for inputs and form controls).
// These effects should only be committed when components are first mounted,
// aka when there is no current/alternate.
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
// commitMount 处理 input 标签有 auto-focus 的情况
commitMount(instance, type, props, finishedWork);
}
break;
}
// ...
}
}
// 更新 ref 引用
if (!enableSuspenseLayoutEffectSemantics || !offscreenSubtreeWasHidden) {
if (enableScopeAPI) {
// TODO: This is a temporary solution that allowed us to transition away
// from React Flare on www.
if (finishedWork.flags & Ref && finishedWork.tag !== ScopeComponent) {
commitAttachRef(finishedWork);
}
} else {
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
}
}
}
在 commitLayoutEffectOnFiber 函数中,会根据组件的类型,执行不同的处理。
如果是 FunctionComponent、ForwardRef、SimpleMemoComponent 等组件,则调用 commitHookEffectListMount 函数,循环effect链表,执行effect副作用。
如果是 ClassComponent 组件,则会针对首次渲染和非首次渲染分别执行 componentDidMount 和 componentDidUpdate 生命周期函数,并调用commitUpdateQueue函数,遍历存储在 updateQueue上的effects,执行 effect 副作用。
如果是 HostComponent 组件,则调用 commitMount 处理 input 标签有 auto-focus 的情况。
commitUpdateQueue -- 处理回调
// react-reconciler/src/ReactUpdateQueue.new.js
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
): void {
// Commit the effects
// 遍历effects 列表,执行effect副作用
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);
}
}
}
}
function callCallback(callback, context) {
if (typeof callback !== 'function') {
throw new Error(
'Invalid argument passed as callback. Expected a function. Instead ' +
`received: ${callback}`,
);
}
callback.call(context);
}
存储在 updateQueue 上的 effects 副作用是通过 commitUpdateQueue 函数来执行的。在该函数中,会对 effects 进行遍历,若有callback,则执行callback,同时会重置 updateQueue 上的 effects 为 null 。
至此,layout phase 阶段的工作就完成了。
总结
本文深入解读了commit阶段的三个阶段:before mutation阶段,mutation phase 阶段、layout phase 阶段。这三个阶段主要做的事情在《commitRootImpl》小节中已有介绍,此处不再赘述。