Written at the beginning of the column (stack armor)
-
The author is not an expert in front-end technology, but just a novice in front-end technology who likes to learn new things. He wants to learn source code just to cope with the rapidly declining front-end market and the need to find a job. This column is the author's own thinking in the process of learning And experience, there are also many parts that refer to other tutorials. If there are errors or problems, please point them out to the author. The author guarantees that the content is 100% correct. Please do not use this column as a reference answer.
-
The reading of this column requires you to have a certain foundation of React, JavaScript and front-end engineering. The author will not explain many basic knowledge points, such as: what is babel, what is the syntax of jsx, please refer to it when necessary material.
-
Many parts of this column refer to a large number of other tutorials. If there are similarities, the author plagiarized them. Therefore, this tutorial is completely open source. You can integrate, summarize and add your own understanding to various tutorials as the author.
Contents of this section
In this section, we will talk about the operation of React in commit , because in the previous tutorial, we have already talked about how React turns the element node into a Fiber tree, and how to update our Fiber tree through the DIFF algorithm, so now we need Synchronize the update of our Fiber tree to our real DOM and show it to the user. This stage is the Commit stage
from the last stage
Now let's turn our attention back to our fourth tutorial - updateContainer
the relevant part, we can see the two functions we mentioned before: performConcurrentWorkOnRoot
, performSyncWorkOnRoot
, let's take a look at their structure:
- Their logic is roughly the same, except that they
ConcurrentWork
need to deal with the process scheduling and priority issues related to concurrent tasks - Through the study of tutorials 5 and 6, we know that in
renderRootSync
andrenderRootConcurrent
, we generated our Fiber tree, so after these two functions, we can get our Fiber tree - After going through a series of error handling functions (probably something wrong with generating our Fiber) we finally arrive at
commitRoot
this function, which passes in the structure of the Fiber tree we just generated. Among them, the sync task is called directly, and the Concurrent task needs to passfinishConcurrentRender
this function to determine whether our rendering is completed according to the state, and then callcommitRootWhenReady
(which calls commitRoot) to enter ourcommit
stage (about the difference between these two tasks, we will Separately later)
export function performSyncWorkOnRoot(root: FiberRoot): null {
// 省略....
flushPassiveEffects();
// 优先级相关
let lanes = getNextLanes(root, NoLanes);
if (!includesSyncLane(lanes)) {
ensureRootIsScheduled(root);
return null;
}
// 生成 Fiber 的函数
let exitStatus = renderRootSync(root, lanes);、
//下面都是处理生成失败的处理
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
//...
}
if (exitStatus === RootFatalErrored) {
//...
}
if (exitStatus === RootDidNotComplete) {
//...
}
// 生成完毕,返回我们的 WorkInProgress 树的根节点
const finishedWork: Fiber = (root.current.alternate: any);
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 进入 commit 阶段
commitRoot(
root,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
);
ensureRootIsScheduled(root);
return null;
}
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean,
): RenderTaskFn | null {
// 省略处理优先级相关、调度相关的 ....
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
// 生成 Fiber 的函数
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
//下面都是处理生成失败的处理....省略
// 生成完毕,返回我们的 WorkInProgress 树的根节点
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
// 进入 commit 阶段
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}
// finishConcurrentRender 判定是不是可以进入 commit 阶段,具体的分析之后我们会单独开一篇
function finishConcurrentRender(
root: FiberRoot,
exitStatus: RootExitStatus,
finishedWork: Fiber,
lanes: Lanes,
) {
// 根据状态判定,
switch (exitStatus) {
case RootInProgress:
case RootFatalErrored: {
throw new Error('Root did not complete. This is a bug in React.');
}
case RootErrored: {
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
break;
}
case RootSuspended: {
// 省略....
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
break;
}
case RootSuspendedWithDelay: {
// 省略....
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
break;
}
case RootCompleted: {
commitRootWhenReady(
root,
finishedWork,
workInProgressRootRecoverableErrors,
workInProgressTransitions,
lanes,
);
break;
}
default: {
throw new Error('Unknown root exit status.');
}
}
}
commitRoot
So it can be seen that commitRoot
this function is the unified entry of the function after we generate our Fiber, and it calls commitRootImpl
this function again. Let's see what this function does now. First of all, it needs to be clear that a one-way linked list effectList of Fiber nodes that need to perform side effects is saved on rootFiber.firstEffect, and the changed props are saved in the updateQueue of these Fiber nodes:
- It first executes the useEffect callback and other synchronous tasks as they may trigger a new render
- Then assign values to our variables and reset the FiberRoot state
- After that, it traverses the effectList three times, processing the three stages of BeforeMutationEffects, MutationEffects and LayoutEffects respectively:
BeforeMutationEffects
Mainly update the state, props, etc. on the class component instance, and execute the getSnapshotBeforeUpdate life cycle functionMutationEffects
It mainly completes the execution of side effects, mainly including resetting text nodes and inserting, deleting and updating real dom nodes.LayoutEffects
Mainly to trigger componentDidMount, componentDidUpdate and various callback functions, etc.
- Finally, it also needs to deal with useEffect-related content, because some life cycle hooks will be triggered during the commit phase, so related synchronization tasks must also be performed
function commitRoot(
root: FiberRoot,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
) {
const previousUpdateLanePriority = getCurrentUpdatePriority();
const prevTransition = ReactCurrentBatchConfig.transition;
try {
ReactCurrentBatchConfig.transition = null;
setCurrentUpdatePriority(DiscreteEventPriority);
commitRootImpl(
root,
recoverableErrors,
transitions,
previousUpdateLanePriority,
);
} finally {
ReactCurrentBatchConfig.transition = prevTransition;
setCurrentUpdatePriority(previousUpdateLanePriority);
}
return null;
}
function commitRootImpl(
root: FiberRoot,
recoverableErrors: null | Array<mixed>,
transitions: Array<Transition> | null,
renderPriorityLevel: EventPriority,
) {
// 调用flushPassiveEffects执行完所有effect的任务
do {
flushPassiveEffects();
} while (rootWithPendingPassiveEffects !== null);
// 优先级调度
const finishedWork = root.finishedWork;
const lanes = root.finishedLanes;
if (enableSchedulingProfiler) {
markCommitStarted(lanes);
}
if (finishedWork === null) {
// 异常处理
}
//重置 FiberRoot
root.finishedWork = null;
root.finishedLanes = NoLanes;
root.callbackNode = null;
root.callbackPriority = NoLane;
//省略优先级相关的....
// 重置全局变量
if (root === workInProgressRoot) {
workInProgressRoot = null;
workInProgress = null;
workInProgressRootRenderLanes = NoLanes;
} else {
}
// 处理 useEffect相关内容
if (
(finishedWork.subtreeFlags & PassiveMask) !== NoFlags ||
(finishedWork.flags & PassiveMask) !== NoFlags
) {
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
pendingPassiveEffectsRemainingLanes = remainingLanes;
pendingPassiveTransitions = transitions;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
}
// 第一次遍历,执行 commitBeforeMutationEffects
const subtreeHasEffects =
(finishedWork.subtreeFlags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
const rootHasEffect =
(finishedWork.flags &
(BeforeMutationMask | MutationMask | LayoutMask | PassiveMask)) !==
NoFlags;
if (subtreeHasEffects || rootHasEffect) {
const prevTransition = ReactCurrentBatchConfig.transition;
ReactCurrentBatchConfig.transition = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
ReactCurrentOwner.current = null;
const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
root,
finishedWork,
);
if (enableProfilerTimer) {
recordCommitTime();
}
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
rootCommittingMutationOrLayoutEffects = root;
}
// 第二次遍历,执行 commitMutationEffects
commitMutationEffects(root, finishedWork, lanes);
if (enableCreateEventHandleAPI) {
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
}
resetAfterCommit(root.containerInfo);
root.current = finishedWork;
if (enableSchedulingProfiler) {
markLayoutEffectsStarted(lanes);
}
// 第三次遍历,执行 commitLayoutEffects
commitLayoutEffects(finishedWork, root, lanes);
if (enableSchedulingProfiler) {
markLayoutEffectsStopped();
}
if (enableProfilerTimer && enableProfilerNestedUpdateScheduledHook) {
rootCommittingMutationOrLayoutEffects = null;
}
requestPaint();
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactCurrentBatchConfig.transition = prevTransition;
} else {
// 没有任何副作用
root.current = finishedWork;
if (enableProfilerTimer) {
recordCommitTime();
}
}
const rootDidHavePassiveEffects = rootDoesHavePassiveEffects;
// 处理 useEffect相关内容
if (rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = false;
rootWithPendingPassiveEffects = root;
pendingPassiveEffectsLanes = lanes;
} else {
releaseRootPooledCache(root, remainingLanes);
}
// 省略优先级相关和DEV模式相关....
// 触发一次新的调度,确保任何附加的任务被调度
ensureRootIsScheduled(root, now());
// 处理componentDidMount等生命周期或者useLayoutEffect等同步任务
flushSyncCallbacks();
return null;
}
BeforeMutationEffects
Let's look at each stage in turn, and at BeforeMutationEffects
this stage, let's see what it does:
-
This stage will call
commitBeforeMutationEffects
the function processing -
commitBeforeMutationEffects
The function will be called in turncommitBeforeMutationEffects_begin
,commitBeforeMutationEffects_complete
,commitBeforeMutationEffectsOnFiber
,getSnapShotBeforeUpdate
function, which triggersgetSnapShotBeforeUpdate
this life cycle -
commitBeforeMutationEffects_begin
In the function, the children that need to be deleted in the current stage (generated in the diff of the previous stage) are obtained, and then related operations are performed on them, mainly processingfocus
andblur
. Then traverse deeply until there are no children -
commitBeforeMutationEffects_complete
When the node we traverse has no children, we traverse its brothers, that is to say,commitBeforeMutationEffects_begin
the function andcommitBeforeMutationEffects_complete
the function traverse our Fiber together (the traversal method is roughly the same as the previous traversal element) -
commitBeforeMutationEffectsOnFiber
It is used to process each Fiber, mainly to process the ClassComponent component, update the state, props, etc. on the instance, and execute the getSnapshotBeforeUpdate lifecycle function:
export function commitBeforeMutationEffects(
root: FiberRoot,
firstChild: Fiber,
) {
// 准备提交
focusedInstanceHandle = prepareForCommit(root.containerInfo);
nextEffect = firstChild;
// 开始提交
commitBeforeMutationEffects_begin();
const shouldFire = shouldFireAfterActiveInstanceBlur;
shouldFireAfterActiveInstanceBlur = false;
focusedInstanceHandle = null;
return shouldFire;
}
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
) {
child.return = fiber;
nextEffect = child;
} else {
// 提交结束
commitBeforeMutationEffects_complete();
}
}
}
// 提交完毕
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
setCurrentDebugFiberInDEV(fiber);
try {
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
resetCurrentDebugFiberInDEV();
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
//...省略
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;
//触发 getSnapshotBeforeUpdate
const snapshot = instance.getSnapshotBeforeUpdate(
finishedWork.elementType === finishedWork.type
? prevProps
: resolveDefaultProps(finishedWork.type, prevProps),
prevState,
);
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
}
break;
}
case HostRoot: {
if (supportsMutation) {
const root = finishedWork.stateNode;
clearContainer(root.containerInfo);
}
break;
}
case HostComponent:
case HostText:
case HostPortal:
case IncompleteClassComponent:
break;
default: {
throw new Error(
'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();
}
}
MutationEffects
The previous stage just did some preparatory work, and at MutationEffects
this stage, we need to synchronize our modifications to our DOM. Let's take a look at what it does:
- Different types of fibers
MutationEffects
will be processed differently at this stage, but some common logic will be executed, that is, delete operations and insert operations - The first is to call
recursivelyTraverseMutationEffects
the method , which will execute the deletion logic. After the deletion logic is completed, - Then there is the call
commitReconciliationEffects
, which is responsible for inserting DOM nodes into the real DOM tree - Finally, we do different operations according to different components
export function commitMutationEffects(
root: FiberRoot,
finishedWork: Fiber,
committedLanes: Lanes,
) {
inProgressLanes = committedLanes;
inProgressRoot = root;
setCurrentDebugFiberInDEV(finishedWork);、
//直接进入 commitMutationEffectsOnFiber
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
setCurrentDebugFiberInDEV(finishedWork);
inProgressLanes = null;
inProgressRoot = null;
}
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
) {
// 获取 current 树的内容
const current = finishedWork.alternate;
// 获取元素被打上的标记
const flags = finishedWork.flags;
// 根据不同类别的元素做不同的擦操作
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// 删除操作
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// 插入操作
commitReconciliationEffects(finishedWork);
// ....
}
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ....
return;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ....
return;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ....
return;
}
case HostRoot: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
// ....
return;
}
// .... 省略
default: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
return;
}
}
}
Let's first look at the operation recursivelyTraverseMutationEffects
of , which obtains deletions
the children stored in in the previous stage, and then calls commitDeletionEffects
the method , which in turn calls commitDeletionEffectsOnFiber
this function. This function needs to be discussed in categories:
- For native components we do this:
- First set the element pointed to by its ref to null
- Traverse down the child nodes and execute the deletion logic
- Finally call the removeChild method to delete the real DOM
- For class components
- First set the element pointed to by its ref to null
- and then call its
componentWillUnmount
lifecycle function - Traverse the child nodes down and execute the deletion logic. Because it does not correspond to the real DOM, it does not need to be deleted. At the same time, its child nodes are all other components or real native tags, so you only need to delete the child nodes recursively
- For function components
- We need to traverse its updateQueue to execute the function returned by the callback function of useInsertionEffect / useLayoutEffect (handling side effects)
- Traverse down the child nodes and execute the deletion logic
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// 取出要删除的孩子
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 删除孩子
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
}
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
onCommitUnmount(deletedFiber);
switch (deletedFiber.tag) {
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
// ref 设置回 null
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
}
case HostText: {
if (supportsMutation) {
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
// 往下遍历子节点,执行删除
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
// 删除真正的 DOM,调用了原生的 removeChild 方法
if (hostParent !== null) {
if (hostParentIsContainer) {
removeChildFromContainer(
((hostParent: any): Container),
(deletedFiber.stateNode: Instance | TextInstance),
);
} else {
removeChild(
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
}
} else {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
}
return;
}
// ....
// 函数组件
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
// 读取 updateQueue 队列,队列用链表的方式保存
const updateQueue: FunctionComponentUpdateQueue | null = (deletedFiber.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 & HookInsertion) !== NoHookEffect) {
// 处理 useInsertionEffect 副作用
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
} else if ((tag & HookLayout) !== NoHookEffect) {
// 处理 useLayoutEffect 副作用
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStarted(deletedFiber);
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
deletedFiber.mode & ProfileMode
) {
startLayoutEffectTimer();
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
recordLayoutEffectDuration(deletedFiber);
} else {
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
}
if (enableSchedulingProfiler) {
markComponentLayoutEffectUnmountStopped();
}
}
}
effect = effect.next;
} while (effect !== firstEffect);
}
}
}
// 遍历子节点执行删除逻辑
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
// 类组件
case ClassComponent: {
if (!offscreenSubtreeWasHidden) {
// 移除 ref
safelyDetachRef(deletedFiber, nearestMountedAncestor);
const instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
// 调用类组件实例的 componentWillUnmount 方法
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance,
);
}
}
//遍历子节点执行删除逻辑
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
// ....省略
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}
Then let's take a look at another insertion logic commitReconciliationEffects
, which only operates on native components and fiber root nodes, and does not operate on function components and class components. Its logic is:
- Determine whether the parent Fiber of the node needs to be reset, and if so, call
parent.textContent = ''
to reset its content - Then find out whether it has siblings, if so, call
insertBefore
the method to insert the content before the sibling node, if not, call the parent node'sappendChild
to insert
function commitReconciliationEffects(finishedWork: Fiber) {
const flags = finishedWork.flags;
if (flags & Placement) {
try {
//执行操作
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// 移除 Placement 标志
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
function commitPlacement(finishedWork: Fiber): void {
if (!supportsMutation) {
return;
}
//获取父 fiber
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostComponent: {
const parent: Instance = parentFiber.stateNode;
// 判断父 fiber 是否有 ContentReset(内容重置)标记
if (parentFiber.flags & ContentReset) {
// 通过 parent.textContent = '' 的方式重置
resetTextContent(parent);
parentFiber.flags &= ~ContentReset;
}
// 找它的下一个兄弟 DOM 节点,
const before = getHostSibling(finishedWork);
// 如果存在,用 insertBefore 方法;如果没有,就调用原生的 appendChild 方法
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
break;
}
default:
throw new Error(
'Invalid host parent fiber. This error is likely caused by a bug ' +
'in React. Please file an issue.',
);
}
}
Now let's go back to ours commitMutationEffectsOnFiber
, let's take a look at what the three different nodes did after the deletion and insertion operations:
- For our class components, we don't handle
- For our function components we handle side effects of useInsertionEffect, useLayoutEffect
- For native components, if they can be reused, we call to
commitUpdate
execute the update logic, otherwise directly reset the native node - If it is native text, we call
commitTextUpdate
the function to update its text content
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// ....
if (flags & Update) {
try {
// 找出 useInsertionEffect 的 destroy 方法去调用
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
// 执行 useInsertionEffect 的回调函数,并将返回值保存到 effect.destory 里。
commitHookEffectListMount(
HookInsertion | HookHasEffect,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
if (
enableProfilerTimer &&
enableProfilerCommitHooks &&
finishedWork.mode & ProfileMode
) {
try {
startLayoutEffectTimer();
// 执行 useLayoutEffect 对应的 destroy 方法
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
recordLayoutEffectDuration(finishedWork);
} else {
try {
commitHookEffectListUnmount(
HookLayout | HookHasEffect,
finishedWork,
finishedWork.return,
);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
// 类组件不会进行操作
case ClassComponent: {
// ...
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
//原生组件
case HostComponent: {
//...
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
if (supportsMutation) {
// 判断是不是需要重置
if (finishedWork.flags & ContentReset) {
const instance: Instance = finishedWork.stateNode;
try {
resetTextContent(instance);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
// 判断是不是需要更新
if (flags & Update) {
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) {
try {
// 更新操作
commitUpdate(
instance,
updatePayload,
type,
oldProps,
newProps,
finishedWork,
);
} catch (error) {
captureCommitPhaseError(
finishedWork,
finishedWork.return,
error,
);
}
}
}
}
}
return;
}
//原生文本
case HostText: {
//....
if (flags & Update) {
if (supportsMutation) {
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;
const oldText: string =
current !== null ? current.memoizedProps : newText;
try {
// 更新操作
commitTextUpdate(textInstance, oldText, newText);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
}
}
return;
}
//...
}
Finally, let's take a look at this update function. Its logic is very simple: it obtains the real dom node instance, props and updateQueue, and applies the properties of props to the real DOM one by one
export function commitUpdate(
domElement: Instance,
updatePayload: Array<mixed>,
type: string,
oldProps: Props,
newProps: Props,
internalInstanceHandle: Object,
): void {
// 对 props 的进行对比更新
updateProperties(domElement, updatePayload, type, oldProps, newProps);
// 应用更新
updateFiberProps(domElement, newProps);
}
// 对比更新
export function updateProperties(
domElement: Element,
updatePayload: Array<any>,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
): void {
// 针对表单组件进行特殊处理
if (
tag === 'input' &&
nextRawProps.type === 'radio' &&
nextRawProps.name != null
) {
ReactDOMInputUpdateChecked(domElement, nextRawProps);
}
// 判断是否为用户自定义的组件,即是否包含 "-"
const wasCustomComponentTag = isCustomComponent(tag, lastRawProps);
const isCustomComponentTag = isCustomComponent(tag, nextRawProps);
// 将 diff 结果应用于真实 dom
updateDOMProperties(
domElement,
updatePayload,
wasCustomComponentTag,
isCustomComponentTag,
);
// 针对表单的特殊处理
switch (tag) {
case 'input':
ReactDOMInputUpdateWrapper(domElement, nextRawProps);
break;
case 'textarea':
ReactDOMTextareaUpdateWrapper(domElement, nextRawProps);
break;
case 'select':
ReactDOMSelectPostUpdateWrapper(domElement, nextRawProps);
break;
}
}
function updateDOMProperties(
domElement: Element,
updatePayload: Array<any>,
wasCustomComponentTag: boolean,
isCustomComponentTag: boolean,
): void {
// 对 updatePayload 遍历
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);
}
}
}
LayoutEffects
Finally, let's take a look at LayoutEffects
this stage. At this stage, we can already call our real DOM. It is mainly about execution componentDidMount
and componentDidUpdate
life cycle. Let's take a look at the logic:
- Its traversal process is
BeforeMutationEffects
basically the same as that of . They all use the depth-first traversal method, we will not elaborate here, let's look directly atcommitLayoutEffectOnFiber
this function - It is still processed in components:
- If it is a function component, we can execute its
useLayoutEffect
hooks - If it is a class component, we need to execute its
componentDidMount
orcomponentDidUpdate
life cycle, and then we have to deal with the callback functioncommitUpdateQueue
in - If the root node, we need to handle its callback, which is
ReactDOM.render
the callback function of - If it is a native component, we handle its automatic update
- If it is a function component, we can execute its
commitUpdateQueue
The function will traverse the effects on the finishedQueue, and if there is a callback, execute the callback. At the same time, the effects on the finishedQueue will be reset to null
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();
// 首次渲染,触发 componentDidMount 生命周期
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,
);
}
}
}
}
const updateQueue: UpdateQueue<
*,
> | null = (finishedWork.updateQueue: any);
if (updateQueue !== null) {
// 执行 commitUpdateQueue 处理回调
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
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;
}
}
// 调用 commitUpdateQueue 处理 ReactDOM.render 的回调
commitUpdateQueue(finishedWork, updateQueue, instance);
}
break;
}
case HostComponent: {
const instance: Instance = finishedWork.stateNode;
// commitMount 处理 input 标签有 auto-focus 的情况
if (current === null && finishedWork.flags & Update) {
const type = finishedWork.type;
const props = finishedWork.memoizedProps;
commitMount(instance, type, props, finishedWork);
}
break;
}
case HostText:
break;
}
case HostPortal: {
break;
}
default:
throw new Error(
'This unit of work tag should not have side-effects. This error is ' +
'likely caused by a bug in React. Please file an issue.',
);
}
}
}
Summarize
The above is the content of our commit stage. Let’s summarize its operation process, which can be roughly divided into five stages.
- Before entering
BeforeMutationEffects
the function, we first processuseEffect
related tasks and do some initialization work - After entering
BeforeMutationEffects
the stage , we traverse our Fiber tree in depth first, processfocus
the andblur
logic after DOM node rendering/deletion, and then processgetSnapshotBeforeUpdate
the life - Then we enter
MutationEffects
the stage . In this stage, we executerecursivelyTraverseMutationEffects
the method to process the nodes that need to be deleted, and then executecommitReconciliationEffects
the logic to insert into the DOM, and then process them separately- For our function components we handle side effects of useInsertionEffect, useLayoutEffect
- For native components and text, if calling
commitUpdate
execute update logic (reuse)
- Finally, we enter
LayoutEffects
the stage , mainly to execute the hooks of class componentscomponentDidMount
andcomponentDidUpdate
life cycle or function componentsuseLayoutEffect
, and then process the callbacks of the effects on the finishedQueue - After the end of the three phases, process the
useEffect
relevant , and then execute the synchronization tasks related to the commit phase
After the above operations, the React nodes are synchronized from the Fiber we built to the real DOM and displayed to the user, and the related life cycle and Hooks are also triggered. So far, a React rendering process starting from jsx has been finished. We will sort it out completely after we finish talking about scheduling.
Now let's take a look at what is still unresolved:
- In the previous explanation, we skipped the content related to priority and scheduling by default. This is actually a very important stage. After that, we
scheduler
will combine our previous explanation of the rendering process to explainscheduler
how arrange tasks and schedule processes. of - We have mentioned part of the content of hooks, but we haven't systematically talked about how hooks work. We will open a few separate articles later to talk about how our hooks are mounted and take effect
Then we will answer these two questions in the following tutorials.