通俗易懂的React原理(八):commit流程(上)

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0 ,并且经过简化,仅保留了关键的逻辑,以便于理解流程

前面我们基本讲完了render阶段,也就是React生成新的fiber tree的过程。后面我们就该看commit阶段了,这是根据新的Fiber tree去修改真实dom的过程。

我们之前说的可中断,也仅仅是针对render阶段的。因为render阶段,只是修改内存中,后缓冲区的那个workInProgress fiber tree。而commit阶段,则是要去修改dom了,为了保证页面上展示的不是半新半老的,commit阶段是不可被打断,同步执行下来的。

commit阶段的入口,就在performWorkOnRoot里面,当渲染逻辑结束后,如果一次渲染正常结束,会执行finishConcurrentRender方法,进而沿着finishConcurrentRender->commitRootWhenReady->commitRoot这个路径,进入到commitRoot的逻辑里。

commitRoot方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
function commitRoot(
root: FiberRoot,
finishedWork: null | Fiber,
lanes: Lanes,
recoverableErrors: null | Array<CapturedValue<mixed>>,
transitions: Array<Transition> | null,
didIncludeRenderPhaseUpdate: boolean,
spawnedLane: Lane,
updatedLanes: Lanes,
suspendedRetryLanes: Lanes,
exitStatus: RootExitStatus,
suspendedCommitReason: SuspendedCommitReason, // Profiling-only
completedRenderStartTime: number, // Profiling-only
completedRenderEndTime: number, // Profiling-only
): void {

do {
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
// means `flushPassiveEffects` will sometimes result in additional
// passive effects. So we need to keep flushing in a loop until there are
// no more pending effects.
// TODO: Might be better if `flushPassiveEffects` did not automatically
// flush synchronous work at the end, to avoid factoring hazards like this.
flushPendingEffects();
} while (pendingEffectsStatus !== NO_PENDING_EFFECTS);
flushRenderPhaseStrictModeWarningsInDEV(); // Check which lanes no longer have any work scheduled on them, and mark
let passiveSubtreeMask;

passiveSubtreeMask = PassiveMask;

if (
(finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
(finishedWork.flags & passiveSubtreeMask) !== NoFlags
) {
root.callbackNode = null;
root.callbackPriority = NoLane;
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects(true);
// This render triggered passive effects: release the root cache pool
// *after* passive effects fire to avoid freeing a cache pool that may
// be referenced by a node in the tree (HostRoot, Cache boundary etc)
return null;
});
} else {
// If we don't have passive effects, we're not going to need to perform more work
// so we can clear the callback now.
root.callbackNode = null;
root.callbackPriority = NoLane;
}

pendingEffectsStatus = PENDING_MUTATION_PHASE;
// Flush synchronously.
flushMutationEffects();
flushLayoutEffects();
// Skip flushAfterMutationEffects
flushSpawnedWork();
}

commitRoot的核心代码大致如上所示。其中finishedWork这个参数,后面也会多次出现,对应的是render阶段的workInProgress,都表示当前遍历到,正在处理的那个Fiber节点。

首先要保证执行下面的代码时,没有pending状态的副作用(注释里写了为什么可能这时会有副作用),所以需要不停地执行flushPendingEffects,里面会把所有类型的副作用都执行一遍。

然后,如果整个fiber树中有passive类型的副作用(即useEffect产生的副作用),那么会通过Scheduler,注册一个宏任务,在下一个宏任务里执行整棵树上所有的passive类型副作用。

然后我们跳过了一个beforeMutationEffect的处理,因为这个跟函数组件没什么关系了,所以不是我们关注的重点。

最后,按顺序处理三个任务:MutationEffect类型的副作用,LayoutEffect类型的副作用,以及处理前面的任务过程中,新出现的任务即SpawnedWork

其整体流程简要如下图所示,特别将这一节要讲的mutationEffect内容也展开,便于理解

1
2
3
4
5
6
7
8
9
10
commitRoot
├─ flushPendingEffects
├─ flushMutationEffects
│ └─ commitMutationEffectsOnFiber
│ ├─ recursivelyTraverseMutationEffects
│ │ └─ commitDeletionEffectsOnFiber
│ ├─ commitReconciliationEffects (Placement)
│ └─ Update (hooks/DOM更新)
├─ flushLayoutEffects
└─ flushSpawnedWork

MutationEffect

mutationEffect,即改变dom的effect。删除dom节点,新增dom节点之类的,都是在这个阶段完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
function flushMutationEffects(): void {
if (pendingEffectsStatus !== PENDING_MUTATION_PHASE) {
return;
}
pendingEffectsStatus = NO_PENDING_EFFECTS;

const root = pendingEffectsRoot;
const finishedWork = pendingFinishedWork;
const lanes = pendingEffectsLanes;
const subtreeMutationHasEffects =
(finishedWork.subtreeFlags & MutationMask) !== NoFlags;
const rootMutationHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;

if (subtreeMutationHasEffects || rootMutationHasEffect) {
const prevTransition = ReactSharedInternals.T;
ReactSharedInternals.T = null;
const previousPriority = getCurrentUpdatePriority();
setCurrentUpdatePriority(DiscreteEventPriority);
const prevExecutionContext = executionContext;
executionContext |= CommitContext;
try {
// The next phase is the mutation phase, where we mutate the host tree.
commitMutationEffects(root, finishedWork, lanes);

if (enableCreateEventHandleAPI) {
if (shouldFireAfterActiveInstanceBlur) {
afterActiveInstanceBlur();
}
}
resetAfterCommit(root.containerInfo);
} finally {
// Reset the priority to the previous non-sync value.
executionContext = prevExecutionContext;
setCurrentUpdatePriority(previousPriority);
ReactSharedInternals.T = prevTransition;
}
}

// The work-in-progress tree is now the current tree. This must come after
// the mutation phase, so that the previous tree is still current during
// componentWillUnmount, but before the layout phase, so that the finished
// work is current during componentDidMount/Update.
root.current = finishedWork;
pendingEffectsStatus = PENDING_LAYOUT_PHASE;
}

由上面可见,这段代码的核心逻辑是在commitMutationEffects里的。而commitMutationEffects方法里,几乎就是直接执行了commitMutationEffectsOnFiber方法,所以我们只看commitMutationEffectsOnFiber方法就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function commitMutationEffectsOnFiber(
finishedWork: Fiber,
root: FiberRoot,
lanes: Lanes,
){
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork, lanes);

if (flags & Update) {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
// TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings.
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
commitHookLayoutUnmountEffects(
finishedWork,
finishedWork.return,
HookLayout | HookHasEffect,
);
}
break;
}
case HostComponent: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);

commitReconciliationEffects(finishedWork, lanes);

if (finishedWork.flags & ContentReset) {
commitHostResetTextContent(finishedWork);
}

if (flags & Update) {
const instance: Instance = finishedWork.stateNode;
if (instance != null) {
// Commit the work prepared earlier.
// 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 newProps = finishedWork.memoizedProps;
const oldProps =
current !== null ? current.memoizedProps : newProps;
commitHostUpdate(finishedWork, newProps, oldProps);
}
}
break;
}
case HostText: {
recursivelyTraverseMutationEffects(root, finishedWork, lanes);
commitReconciliationEffects(finishedWork, lanes);

if (flags & Update) {
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;

commitHostTextUpdate(finishedWork, newText, oldText);
}
break;
}
}

commitMutationEffectsOnFiber方法里面,主要是根据fiber.tag的不同类型,做不同的操作。我们主要关注三种类型,即FunctionComponent(函数组件),HostComponent(dom的html标签,如<div>之类的对应的fiber node)和HostText(dom里面的文本节点对应的fiber node)

recursivelyTraverseMutationEffects

我们可以看到,对于三种类型的节点,首先都要执行recursivelyTraverseMutationEffects方法,而后才是真正的当前节点的处理逻辑。所以我们先看一下recursivelyTraverseMutationEffects方法,都干了什么。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function recursivelyTraverseMutationEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
// Deletions effects can be scheduled on any fiber type. They need to happen
// before the children effects have fired.
const deletions = parentFiber.deletions;
if (deletions !== null) {
for (let i = 0; i < deletions.length; i++) {
const childToDelete = deletions[i];
commitDeletionEffects(root, parentFiber, childToDelete);
}
}

if (
parentFiber.subtreeFlags &
(enablePersistedModeClonedFlag ? MutationMask | Cloned : MutationMask)
) {
let child = parentFiber.child;
while (child !== null) {
commitMutationEffectsOnFiber(child, root, lanes);
child = child.sibling;
}
}
}

顾名思义,这是个递归的辅助方法。不难发现,它最后又会调用commitMutationEffectsOnFiber方法,整体上形成了commitMutationEffectsOnFiber -> recursivelyTraverseMutationEffects -> commitMutationEffectsOnFiber这样的递归逻辑。这个递归的含义,就是先处理子节点,然后处理子节点的兄弟节点,最后再处理自身。

我们仔细看这个递归的逻辑,是首先找到当前Fiber下,所有需要被删除的子节点,将他们都删除。然后继续对所有子节点的处理

commitDeletionEffectsOnFiber

commitDeletionEffects里面会调用commitDeletionEffectsOnFiber来完成删除逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
function commitDeletionEffectsOnFiber(
finishedRoot: FiberRoot,
nearestMountedAncestor: Fiber,
deletedFiber: Fiber,
) {
// TODO: Delete this Hook once new DevTools ships everywhere. No longer needed.
onCommitUnmount(deletedFiber);

// The cases in this outer switch modify the stack before they traverse
// into their subtree. There are simpler cases in the inner switch
// that don't modify the stack.
switch (deletedFiber.tag) {
case HostComponent:
case HostText: {
// We only need to remove the nearest host child. Set the host parent
// to `null` on the stack to indicate that nested children don't
// need to be removed.
const prevHostParent = hostParent;
const prevHostParentIsContainer = hostParentIsContainer;
hostParent = null;
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
hostParent = prevHostParent;
hostParentIsContainer = prevHostParentIsContainer;
if (hostParent !== null) {
// Now that all the child effects have unmounted, we can remove the
// node from the tree.
commitHostRemoveChild(
deletedFiber,
nearestMountedAncestor,
((hostParent: any): Instance),
(deletedFiber.stateNode: Instance | TextInstance),
);
}
return;
}
case FunctionComponent:
case ForwardRef:
case MemoComponent:
case SimpleMemoComponent: {
commitHookEffectListUnmount(
HookInsertion,
deletedFiber,
nearestMountedAncestor,
);
commitHookLayoutUnmountEffects(
deletedFiber,
nearestMountedAncestor,
HookLayout,
);
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
default: {
recursivelyTraverseDeletionEffects(
finishedRoot,
nearestMountedAncestor,
deletedFiber,
);
return;
}
}
}

依旧是根据Fiber的不同tag类型,执行不同的逻辑。这里HostComponentHostText执行的逻辑基本是一致的。

recursivelyTraverseDeletionEffects其实又是一个递归逻辑,就是对Fiber的所有子节点,都执行一遍commitDeletionEffects,我们就不展开去看了。

当子节点的删除逻辑执行完之后,会执行commitHostRemoveChild方法,把自己对应的dom删除掉,最终的实现是dom的removeChild方法。

而对于函数组件,则要先处理一下卸载自身时候的hooks的return方法,然后再去删除所有子组件。所以这里我们也可以知道组件卸载的时候,这些hooks的执行顺序,是先父组件,然后再子组件。

其中commitHookEffectListUnmount入参指定了effect的flag是HookInsertion,处理useInsertionEffect的return方法。这个hooks是针对css in js库的开发者提供的方法,我们不做过多关心。

然后commitHookLayoutUnmountEffects,根据入参我们可以看到,是对删除的Fiber节点处理HookLayout类型的effect,对应的就是useLayoutEffect的return方法。

接下来就去递归删除子节点,删除子节点最终会终结在叶子结点,一定是host类型的Fiber上,也就是前面提到的HostComponentHostText,最终会实际删除真正的dom节点。

执行删除以外的mutation

看完了recursivelyTraverseMutationEffects的逻辑,我们继续回来看commitMutationEffectsOnFiber的逻辑。

在递归处理完子节点后,回来处理当前Fiber,需要执行commitReconciliationEffects方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function commitReconciliationEffects(
finishedWork: Fiber,
committedLanes: Lanes,
) {
// Placement effects (insertions, reorders) can be scheduled on any fiber
// type. They needs to happen after the children effects have fired, but
// before the effects on this fiber have fired.
const flags = finishedWork.flags;
if (flags & Placement) {
commitHostPlacement(finishedWork);
finishedWork.flags &= ~Placement;
}
if (flags & Hydrating) {
finishedWork.flags &= ~Hydrating;
}
}

如果当前Fiber的flags包含了Placement(前几篇讲的diff流程中打上的标记,包含了新增和重排序,统一处理为插入),那么就执行commitHostPlacement方法来执行插入,插入后去掉Placement标记。

关于commitHostPlacement里面插入的逻辑,首先需要找到插入到哪里。从当前Fiber的父节点开始向上寻找,找到第一个是host类型的Fiber,记为hostParentFiber。我们只看hostParentFiber.tagHostComponent的场景。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const parent: Instance = hostParentFiber.stateNode;
if (hostParentFiber.flags & ContentReset) {
// Reset the text content of the parent before doing any insertions
resetTextContent(parent);
// Clear ContentReset from the effect tag
hostParentFiber.flags &= ~ContentReset;
}

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.
insertOrAppendPlacementNode(
finishedWork,
before,
parent,
parentFragmentInstances,
);

找到HostComponent对应的实际dom,如果flags包含ContentReset,则清空里面的文本节点内容。react-dom通过这个方法区更改dom的文本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function setTextContent(node: Element, text: string): void {
if (text) {
const firstChild = node.firstChild;

if (
firstChild &&
firstChild === node.lastChild &&
firstChild.nodeType === TEXT_NODE
) {
firstChild.nodeValue = text;
return;
}
}
node.textContent = text;
}

除了可能要清空里面的文本,还要真的执行插入逻辑。通过getHostSibling方法,找到应该插入到哪个节点后面。getHostSibling的逻辑比较长,简要概括就是要找自己在最终dom树上的sibling。

因为Fiber tree和dom tree上的节点并不是一一对应的,fiber tree上的sibling如果不是host类型的节点,最终就不会体现在dom上。所以getHostSibling的逻辑考虑了很多情况,但是简要概括就是根据fiber tree的结构,去找sibling,如果他不是host类型就继续找,可能还会去找叔叔节点的子节点,因为最终dom tree上他们可能是相邻的。并且找到的这个节点自身还不能有Placement标记,因为有这个标记说明它在dom tree中的位置也是不可靠的,它也在等待被插入,不能把它当做锚点来插入。。一定要找到没有发生变化的节点。最终找到的节点被记作before,如果不存在这样一个节点,那么before就是undefined。

找到后,insertOrAppendPlacementNode方法会把当前Fiber插入到before前面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function insertOrAppendPlacementNode(
node: Fiber,
before: ?Instance,
parent: Instance,
parentFragmentInstances: null | Array<FragmentInstanceType>,
): void {
const {tag} = node;
const isHost = tag === HostComponent || tag === HostText;
if (isHost) {
const stateNode = node.stateNode;
if (before) {
insertBefore(parent, stateNode, before);
} else {
appendChild(parent, stateNode);
}
return;
}

const child = node.child;
if (child !== null) {
insertOrAppendPlacementNode(child, before, parent, parentFragmentInstances);
let sibling = child.sibling;
while (sibling !== null) {
insertOrAppendPlacementNode(
sibling,
before,
parent,
parentFragmentInstances,
);
sibling = sibling.sibling;
}
}
}

如果待插入的Fiber是host类型的,那么只要看上半的逻辑就好了。如果存在before节点,那么就通过dom的insertBeforeAPI完成插入。如果before节点不存在,那么就通过dom的appendChildAPI,完成插入。

如果待插入的Fiber不是host类型的,也就是说它本身不对应实际的dom节点,那么就对他自身不做任何处理,而是去递归处理他的子节点,直到找到了host类型的节点,再将子节点们按上半部分的逻辑完成插入。

这就是commitReconciliationEffects方法的逻辑,里面处理了Fiber节点的插入的逻辑

目前我们已经看完了节点删除,和节点插入的逻辑。接下来会继续看commitMutationEffectsOnFiber的逻辑

处理完删除和插入后

函数组件

对于函数组件,我们可以看到还需要执行以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (flags & Update) {
commitHookEffectListUnmount(
HookInsertion | HookHasEffect,
finishedWork,
finishedWork.return,
);
// TODO: Use a commitHookInsertionUnmountEffects wrapper to record timings.
commitHookEffectListMount(HookInsertion | HookHasEffect, finishedWork);
commitHookLayoutUnmountEffects(
finishedWork,
finishedWork.return,
HookLayout | HookHasEffect,
);
}

如果这个Fiber的flags包含Update,那么说明这个组件进行了重新渲染,需要重新执行它里面的hooks。注意前面删除节点的时候,我们已经执行了删除节点的useInsertionEffectuseLayoutEffecthooks的return方法。这里更新节点的时候,要先执行useInsertionEffecthooks的return方法。

这里值得注意的是,更新流程和删除流程,父子节点,执行hooks的return方法的顺序是反过来的,更新流程里优先执行子节点的。

执行完return方法,会立刻执行下一轮次的useInsertionEffect的副作用,并且执行上一轮和useLayoutEffect的return方法。

HostComponent和HostText

对于HostText,逻辑更简单一点。执行commitHostTextUpdate方法,替换里面的文本,实际就是直接替换文本节点里面的nodeValue,更新为新的字符串。

对于HostComponent,则是更新这个dom上面的各种字段,例如name, value, defaultValue之类的。详见updateProperties方法

结语

以上就是commitMutationEffectsOnFiber的逻辑,也是commit阶段,处理mutation阶段的逻辑。

下面几篇,我们会继续了解commitRoot余下的逻辑,包括处理layoutEffect,和spawnedWork,以及处理passiveEffect


通俗易懂的React原理(八):commit流程(上)
https://miku03090831.github.io/2025/09/27/通俗易懂的React原理(八):commit流程(上)/
作者
qh_meng
发布于
2025年9月27日
许可协议