
大家好,我是前端西瓜哥。今天我們來詳細講解一下 React 的 commit 階段的邏輯。
React 版本為 18.2.0
commit 分三個階段:
- BeforeMutation。
- Mutation:在這里更新 DOM。
- Layout。
commitRootImpl 中的三個函數的調用分別對應這個三個階段:
function commitRootImpl(){
// BeforeMutation 階段
commitBeforeMutationEffects(root, finishedWork);
// Mutation 階段
commitMutationEffects(root, finishedWork, lanes);
// Layout 階段
commitLayoutEffects(finishedWork, root, lanes);
}
一些標記
在 reconcil (調和)階段,給 fiber 打了很多的 flags(標記),commit 階段是會讀取這些 flags 進行不同的操作的。
flags 是通過二進制掩碼的方式來保存的,掩碼優(yōu)點是節(jié)省內存,缺點是可讀性很差。
使用或位運算,可以將多個 flag 組合成一個組。
我這三個階段 用到的組合掩碼 為:
export const BeforeMutationMask =
Update |
Snapshot;
export const MutationMask =
Placement |
Update |
ChildDeletion |
ContentReset |
Ref |
Hydrating |
Visibility;
export const LayoutMask = Update | Callback | Ref | Visibility;
BeforeMutation 階段
BeforeMutation 階段。
commitRootImpl 首先會 調用 commitBeforeMutationEffects 方法。
commitBeforeMutationEffects 的核心實現:
function commitBeforeMutationEffects(root, firstChild) {
nextEffect = firstChild;
commitBeforeMutationEffects_begin();
}
主要是調用這個 commitBeforeMutationEffects_begin 方法。
begin
begin 干了啥?
進行深度優(yōu)先遍歷,找到最后一個帶有 BeforeMutation 標識的 fiber。這是因為 useEffect 的調用邏輯是從子到父,要找最后一個 fiber 作為起點。
commitBeforeMutationEffects_begin 的核心實現:
function commitBeforeMutationEffects_begin() {
while (nextEffect !== null) {
const fiber = nextEffect;
// 取出子 fiber
const child = fiber.child;
if (
(fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
child !== null
) {
// 說明子 fiber 樹中存在 BeforeMutationMask 標識的 fiber
// 那就繼續(xù)遍歷往下找
child.return = fiber;
nextEffect = child;
} else {
// 找不到了,說明到底了,執(zhí)行 complete 邏輯。
commitBeforeMutationEffects_complete();
}
}
}
subtreeFlags 是當前 fiber 的子樹的標識匯總,目的是防止無意義的完整深度遍歷,能夠更早地結束遍歷。如果直接用 flags,是要遍歷到葉子節(jié)點才能知道到底誰是要找的最有一個節(jié)點。
找到后,調用 complete 。
complete
commitBeforeMutationEffects_complete 實現為:
function commitBeforeMutationEffects_complete() {
while (nextEffect !== null) {
const fiber = nextEffect;
try {
// BeforeMutation 階段真正做的事情
commitBeforeMutationEffectsOnFiber(fiber);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
const sibling = fiber.sibling;
if (sibling !== null) { // 沒有下一個兄弟節(jié)點
sibling.return = fiber.return;
nextEffect = sibling;
return;
// 結束后會回到 begin 中的循環(huán)中
// 繼續(xù)往下找最后一個 帶有 BeforeMutation 標識的 fiber
}
// 從上往下,處理
nextEffect = fiber.return;
}
}
前面很多邏輯都是遍歷的邏輯,真正的核心操作在 commitBeforeMutationEffectsOnFiber 方法。
做了什么?
對標記了 Snapshot 的組件進行處理,通常是類組件,會 調用類組件實例 instance 的 getSnapshotBeforeUpdate 方法,生成快照對象,然后再放到 instance.__reactInternalSnapshotBeforeUpdate 下,作為之后的 componentDidUpdate 鉤子函數的第三個參數。
其他類型的組件基本啥都不做。
function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
// flags 存在 Snapshot
if ((flags & Snapshot) !== NoFlags) {
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.',
);
}
}
}
}
Mutation 階段
mutation 階段是最重要的階段,在這個階段,React 真正地更新了文檔 DOM 樹。
入口函數是 commitMutationEffects,但它只是 commitMutationEffectsOnFiber 的封裝。
function commitMutationEffects(root, finishedWork, committedLanes) {
inProgressLanes = committedLanes;
inProgressRoot = root;
commitMutationEffectsOnFiber(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
每個 fiber 都要傳入 commitMutationEffectsOnFiber,執(zhí)行 mutation 主邏輯。
從這調用??芍?commitMutationEffectsOnFiber 遞歸調用了多次,形成了很長的調用棧。

commitMutationEffectsOnFiber 的核心實現為:
function commitMutationEffectsOnFiber(finishedWork, root, lanes) {
const current = finishedWork.alternate;
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
// 做了一些事情
commitReconciliationEffects(finishedWork);
}
// 類組件
case ClassComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork);
if (flags & Ref) {
if (current !== null) {
safelyDetachRef(current, current.return);
}
}
return;
}
}
對不同類型的 fiber 會進行不同的處理,但有一些公共邏輯會執(zhí)行的,那就是:
// Deletion 深度遍歷執(zhí)行刪除操作
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
// Placement 插入邏輯
commitReconciliationEffects(finishedWork);
刪除邏輯
首先是調用 recursivelyTraverseMutationEffects 方法,這個方法會執(zhí)行刪除邏輯。
該方法會讀取 fiber 的 deletions 數組,對這些要刪除的 fiber 進行操作。
function recursivelyTraverseMutationEffects(root, parentFiber, lanes) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects hae fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
try {
// 執(zhí)行 fiber 的刪除邏輯
commitDeletionEffects(root, parentFiber, childToDelete);
} catch (error) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
}
// 【下面的可不看】其實就是對子節(jié)點遍歷,也執(zhí)行 mutation 主邏輯。
if (parentFiber.subtreeFlags & MutationMask) {
let child = parentFiber.child;
while (child !== null) {
// 又調用 mutation 邏輯的入口函數
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}
對于要刪除的 fiber,我們這里討論原生組件、類組件、函數組件這 3 種組件類型 fiber 的刪除邏輯。
【1】原生組件
對于原生組件類型(div、span 這些):
- 首先將 綁定的 ref 置為 null。
- 先遞歸,對它的子 fiber 調用刪除邏輯。
- 然后 從 DOM 樹中刪除對應 DOM。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那個要被刪除的 fiber
switch (deletedFiber.tag) {
/********* 原生組件 *********/
case HostComponent: {
if (!offscreenSubtreeWasHidden) {
// ref 設置回 null
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
// 后面的 HostText 會接著執(zhí)行,switch 就是這個邏輯
}
case HostText: {
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
// 往下遍歷子節(jié)點,執(zhí)行刪除
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),
);
}
}
return;
}
// 其他組件類型
}
【2】類組件
對于類組件:
- 先重置 ref。
- 然后 調用 componentWillUnmount 方法。
- 最后遞歸,對它的子 fiber 調用刪除邏輯。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那個要被刪除的 fiber
switch (deletedFiber.tag) {
// ...
/********* 類組件 *********/
case ClassComponent: {
if (!offscreenSubtreeWasHidden) {
// 移除 ref
safelyDetachRef(deletedFiber, nearestMountedAncestor);
const instance = deletedFiber.stateNode;
if (typeof instance.componentWillUnmount === 'function') {
// 調用類組件實例的 componentWillUnmount 方法
safelyCallComponentWillUnmount(
deletedFiber,
nearestMountedAncestor,
instance,
);
}
}
// 遍歷子節(jié)點執(zhí)行刪除邏輯
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
// ...
}
}
【3】函數組件
對于函數組件:
- 遍歷它的 updateQueue 隊列,并通過 effect 的 tag 來識別類型來決定是否調用 destory 方法。對 useInsertionEffect 和 useLayoutEffect,調用它們的 destory 方法。destroy 就是執(zhí)行 useInsertionEffect / useLayoutEffect 的回調函數所返回的函數。useEffect 則跳過,不調用 destory 方法。
- 最后遞歸,對它的子 fiber 調用刪除邏輯。
function commitDeletionEffectsOnFiber(finishedRoot, nearestMountedAncestor, deletedFiber) {
// deletedFiber 表示那個要被刪除的 fiber
switch (deletedFiber.tag) {
// ...
/********* 函數組件 *********/
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
if (!offscreenSubtreeWasHidden) {
const updateQueue = deletedFiber.updateQueue;
if (updateQueue !== null) {
// 讀取 updateQueue 隊列,隊列用鏈表的方式保存
const lastEffect = updateQueue.lastEffect;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {destroy, tag} = effect;
if (destroy !== undefined) {
// 處理 useInsertionEffect 產生的副作用
// 執(zhí)行 useInsertionEffect 回調函數返回的函數,即 destroy
if ((tag & HookInsertion) !== NoHookEffect) {
// safelyCallDestroy 只是加了 try-catch 去調用 destroy
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
// useLayoutEffect 同理
} else if ((tag & HookLayout) !== NoHookEffect) {
safelyCallDestroy(
deletedFiber,
nearestMountedAncestor,
destroy,
);
}
}
// 找下一個 effect
effect = effect.next;
} while (effect !== firstEffect);
}
}
}
// 向下遞歸
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}
插入邏輯
完成刪除邏輯后,接著就是調用 commitReconciliationEffects,這個方法負責往真實 DOM 樹中插入 DOM 節(jié)點。
commitReconciliationEffects 核心內容:
function commitReconciliationEffects(finishedWork) {
const flags = finishedWork.flags;
if (flags & Placement) {
try {
// 執(zhí)行 Placement 插入邏輯
commitPlacement(finishedWork);
} catch (error) {
captureCommitPhaseError(finishedWork, finishedWork.return, error);
}
// 移除 Placement 標志
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}
如果 finishedWork 有 Placement 標識,則調用 commitPlacement 方法。
commitPlacement 的邏輯為:
- 如果 finishedWork 還有 ContentReset 標識,先清空標簽體,通過parent.textContent = '' 的方式。
- 接著是執(zhí)行插入邏輯。會嘗試找下一個兄弟節(jié)點,存在會原生的 insertBefore 方法插入,不存在則使用 appendChild 方法。
commitPlacement 實現如下。
function commitPlacement(finishedWork) {
// 獲取父 fiber
const parentFiber = getHostParentFiber(finishedWork);
switch (parentFiber.tag) {
case HostComponent: {
const parent = parentFiber.stateNode;
// 父 fiber 是否有 ContentReset(內容重置)標記
if (parentFiber.flags & ContentReset) {
// 其實就是 parent.textContent = '';
resetTextContent(parent); //
// 移除 ContentReset 標志
parentFiber.flags &= ~ContentReset;
}
// 找它的下一個兄弟 DOM 節(jié)點,后面用 insertBefore 方法
// 如果沒有,就調用原生的 appendChild 方法
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNode(finishedWork, before, parent);
break;
}
case HostRoot:
case HostPortal: {
const parent: Container = parentFiber.stateNode.containerInfo;
const before = getHostSibling(finishedWork);
insertOrAppendPlacementNodeIntoContainer(finishedWork, before, parent);
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.',
);
}
}
Placement 只會在原生組件和 fiber 根節(jié)點上標記,沒有函數組件和類組件什么事。
更新邏輯
對于可復用的原生組件,會 調用 commitUpdate 進行更新。
commitUpdate 的代碼:
function commitUpdate(domElement, updatePayload, type, oldProps, newProps) {
// 對比更新,需要處理 onXx、className 這些特殊的 props
updateProperties(domElement, updatePayload, type, oldProps, newProps);
// 更新 DOM 元素的 "__reactProps$ + randomKey" 為這個新的 props
updateFiberProps(domElement, newProps);
}
?類組件不會進行更新操作。
對于函數組件,會依次調用:
- useInsertionEffect 的回調函數函數返回的銷毀函數(保存在 effect.destroy 中)。
- useInsertionEffect 的回調函數(保存在 effect.create 中),調用完后將返回結果賦值個 effect.destroy,下一次更新再調用。
- useLayoutEffect 的回調函數函數返回的銷毀函數。
需要注意,函數組件初次掛載,flags 也會標記為 Update,走更新邏輯。這也是為什么 useEffect 在函數組件掛載時也會執(zhí)行,和類組件的 componentDidUpate 不同。
// 找出 useInsertionEffect 的 destroy 方法去調用
// 需要注意 destroy 可能為 undefined(函數組件初次掛載的情況下)
commitHookEffectListUnmount(HookInsertion | HookHasEffect, finishedWork, finishedWork.return);
// 執(zhí)行 useInsertionEffect 的回調函數,并將返回值保存到 effect.destory 里。
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
// useLayoutEffect 對應的 destroy 方法
// 同樣可能不存在
commitHookEffectListUnmount(HookLayout | HookHasEffect, finishedWork, finishedWork.return);
Layout
最后是 Layout 階段。
commitLayoutEffects 的實現:
function commitLayoutEffects(finishedWork, root, committedLanes) {
inProgressLanes = committedLanes;
inProgressRoot = root;
nextEffect = finishedWork;
// 又是 begin,和 BeforeMutation 階段類似的遞歸邏輯
commitLayoutEffects_begin(finishedWork, root, committedLanes);
inProgressLanes = null;
inProgressRoot = null;
}
和 BeforeMutation 階段一樣,先深度優(yōu)先遞歸,找最后一個有 LayoutMask 標記的 fiber。
然后從下往上調用 complete 邏輯,確保邏輯是從底部到頂部,即先子后父。
function commitLayoutEffects_begin(subtreeRoot, root, committedLanes) {
// Suspense layout effects semantics don't change for legacy roots.
const isModernRoot = (subtreeRoot.mode & ConcurrentMode) !== NoMode;
while (nextEffect !== null) {
const fiber = nextEffect;
const firstChild = fiber.child;
if (fiber.tag === OffscreenComponent) {
// 離屏組件的邏輯,不講
continue;
}
// 找最后一個有 LayoutMask 標記的 fiber
if ((fiber.subtreeFlags & LayoutMask) !== NoFlags && firstChild !== null) {
firstChild.return = fiber;
nextEffect = firstChild;
} else {
// 到底了,就執(zhí)行 complete
commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes);
}
}
}
complete 代碼:
function commitLayoutMountEffects_complete(subtreeRoot, root, committedLanes) {
while (nextEffect !== null) {
const fiber = nextEffect;
if ((fiber.flags & LayoutMask) !== NoFlags) {
const current = fiber.alternate;
try {
// 調用 commitLayoutEffectOnFiber
commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
} catch (error) {
captureCommitPhaseError(fiber, fiber.return, error);
}
}
if (fiber === subtreeRoot) {
nextEffect = null;
return;
}
const sibling = fiber.sibling;
if (sibling !== null) {
sibling.return = fiber.return;
nextEffect = sibling;
return;
}
nextEffect = fiber.return;
}
}
核心工作在這個 commitLayoutEffectOnFiber 方法。它會根據組件類型不同執(zhí)行不同邏輯。
對于函數組件,會調用 useLayoutEffect 的回調函數(effect.create)。
對于類組件:
- 如果是掛載(通過 fiber.alternate 是否為 null 判斷),調用 instance.componentDidMount 方法。如果是更新,提取 preProps 等參數 傳入到 componentDidUpdate 里調用;
- 取出 updateQueue 里的 effect,依次調用 effect.callback 函數。這個 callback 其實就是 setState 方法的第二個參數。
處理完后,接下來就會 更新 ref :
if (finishedWork.flags & Ref) {
commitAttachRef(finishedWork);
}
操作很簡單,對于原生組件,就是給 fiber.ref.current 賦值為 fiber.stateNode。
useEffect
現在還差 useEffect 沒調用了。
useEffect 不在同步的 commit 階段中執(zhí)行。它是異步的,被 scheduler 異步調度執(zhí)行。
function commitRootImpl(){
// 異步調度
scheduleCallback(NormalSchedulerPriority, () => {
// 執(zhí)行 useEffect
flushPassiveEffects();
return null;
});
// BeforeMutation 階段
commitBeforeMutationEffects(root, finishedWork);
// Mutation 階段
commitMutationEffects(root, finishedWork, lanes);
// Layout 階段
commitLayoutEffects(finishedWork, root, lanes);
}
先執(zhí)行所有 useEffect 的 destroy 方法,然后才執(zhí)行所有 useEffect 的 create 方法。并保持順序是先子后父。
function flushPassiveEffectsImpl() {
// ...
// useEffect 的 destroy
commitPassiveUnmountEffects(root.current);
// useEffect 的 create
commitPassiveMountEffects(root, root.current, lanes, transitions);
// ...
}
流程圖
畫個流程圖:

create 表示傳給 useEffect 的回調函數,destroy 為調用該回調函數返回的銷毀函數。
結尾
總結一下。
commit 分成三個階段:BeforeMuation、Muation 以及 Layout 階段。
- BeforeMuation,沒做太多事,主要是類組件實例調用 getSnapshotBeforeUpdate 生成快照對象保存起來;
- Muation,更新 DOM 的階段,做了刪除、插入、更新操作。(1)刪除邏輯:重置 ref 為 null,根據 fiber.deletions 刪除 DOM 節(jié)點,調用類組件的 componentWillUnmount,調用 useInsertionEffect 和 useLayoutEffect 的 destory 方法(2)插入邏輯:將標記了Place 的節(jié)點進行真實 DOM 的插入(3)對比 props 更新 DOM 節(jié)點,調用 useInsertionEffect 的 destroy 、useInsertionEffect 的 create 和 useLayoutEffect 的 destroy;
- Layout,調用類組件的 componentDidMount、componentDidUpdate、setState 的回調函數;調用函數組件 useLayoutEffect 的 create;最后更新 ref。
最后是 commit 階段外的 useEffect,它被 Scheduler 異步調度執(zhí)行,先執(zhí)行完整棵樹的 destroy,再執(zhí)行完整棵樹的 create。