通俗易懂的React原理(九):commit流程(下)

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

1
2
3
4
5
6
7
function commitRoot(){
// 前略
flushMutationEffects();
flushLayoutEffects();
// Skip flushAfterMutationEffects
flushSpawnedWork();
}

上一篇,我们已经看完了flushMutationEffects的逻辑,那么我们就该继续看flushLayoutEffectsflushSpawnedWork的逻辑了。

flushLayoutEffects

这一部分的主要目的,就是处理useLayoutEffect的逻辑。

和mutation阶段一样,layout这部分的核心逻辑在里层的方法里,名为commitLayoutEffectOnFiber

也是根据fiber.tag的不同类型,执行不同的逻辑。

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
function commitLayoutEffectOnFiber(
finishedRoot: FiberRoot,
current: Fiber | null,
finishedWork: Fiber,
committedLanes: Lanes,
): void {
const flags = finishedWork.flags;
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
if (flags & Update) {
commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect);
}
break;
}
case HostHoistable:
case HostComponent: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);

// 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 && flags & Update) {
commitHostMount(finishedWork);
}

if (flags & Ref) {
safelyAttachRef(finishedWork, finishedWork.return);
}
break;
}
// Fallthrough
default: {
recursivelyTraverseLayoutEffects(
finishedRoot,
finishedWork,
committedLanes,
);
break;
}
}
}

和mutation阶段的逻辑很相似,layout阶段也是先递归执行子节点的逻辑。recursivelyTraverseLayoutEffects方法会遍历子节点,所有的子节点,如果flags包含layout(Update | Callback | Ref | Visibility),则直接对子节点执行commitLayoutEffectOnFiber。比mutation阶段简单的多,不需要先递归删除,逻辑清晰了不少。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function recursivelyTraverseLayoutEffects(
root: FiberRoot,
parentFiber: Fiber,
lanes: Lanes,
) {
if (parentFiber.subtreeFlags & LayoutMask) {
let child = parentFiber.child;
while (child !== null) {
const current = child.alternate;
commitLayoutEffectOnFiber(root, current, child, lanes);
child = child.sibling;
}
}
}

处理函数组件

递归处理子节点完毕后,回到对当前节点的处理。如果当前节点是函数组件,并且flags包含Update,那么就要执行useLayoutEffect里面的逻辑(return的逻辑已经在mutation阶段执行),commitHookLayoutEffects(finishedWork, HookLayout | HookHasEffect)

处理host组件

由于useLayoutEffect只在React组件里才能有,所以host类型的组件是不会有layout阶段的副作用的。在这个阶段,host组件只需要处理一些问题,例如对于autofocus的元素,让它们focus(commitHostMount)。以及将React的ref绑定到dom上。

flushSpawnedWork

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
function flushSpawnedWork(): void {
if (
pendingEffectsStatus !== PENDING_SPAWNED_WORK &&
// If a startViewTransition times out, we might flush this earlier than
// after mutation phase. In that case, we just skip the after mutation phase.
pendingEffectsStatus !== PENDING_AFTER_MUTATION_PHASE
) {
return;
}
pendingEffectsStatus = NO_PENDING_EFFECTS;

const root = pendingEffectsRoot;
const finishedWork = pendingFinishedWork;
const lanes = pendingEffectsLanes;
const didIncludeRenderPhaseUpdate = pendingDidIncludeRenderPhaseUpdate;

const passiveSubtreeMask = PassiveMask;
const rootDidHavePassiveEffects = // If this subtree rendered with profiling this commit, we need to visit it to log it.
(finishedWork.subtreeFlags & passiveSubtreeMask) !== NoFlags ||
(finishedWork.flags & passiveSubtreeMask) !== NoFlags;

if (rootDidHavePassiveEffects) {
pendingEffectsStatus = PENDING_PASSIVE_PHASE;
} else {
pendingEffectsStatus = NO_PENDING_EFFECTS;
pendingEffectsRoot = (null: any); // Clear for GC purposes.
pendingFinishedWork = (null: any); // Clear for GC purposes.
}

// Read this again, since an effect might have updated it
let remainingLanes = root.pendingLanes;

if (
includesSyncLane(pendingEffectsLanes) &&
(disableLegacyMode || root.tag !== LegacyRoot)
) {
flushPendingEffects();
}

// Always call this before exiting `commitRoot`, to ensure that any
// additional work on this root is scheduled.
ensureRootIsScheduled(root);

// Read this again, since a passive effect might have updated it
remainingLanes = root.pendingLanes;

// Check if this render scheduled a cascading synchronous update. This is a
// heurstic to detect infinite update loops. We are intentionally excluding
// hydration lanes in this check, because render triggered by selective
// hydration is conceptually not an update.
if (
(enableInfiniteRenderLoopDetection &&
(didIncludeRenderPhaseUpdate || didIncludeCommitPhaseUpdate)) ||
(includesSomeLane(lanes, UpdateLanes) &&
includesSomeLane(remainingLanes, SyncUpdateLanes))
) {
if (root === rootWithNestedUpdates) {
nestedUpdateCount++;
} else {
nestedUpdateCount = 0;
rootWithNestedUpdates = root;
}
} else {
nestedUpdateCount = 0;
}

flushSyncWorkOnAllRoots();
}

flushSpawnedWork阶段,主要是处理前面阶段里,派生出来的问题。比如在useLayoutEffect里面如果使用了useState的dispatch,那么这个这个dispatch对应的工作就会在flushSpawnedWork阶段被处理。

其实去掉了一堆试验阶段的特性和日志后,这部分的逻辑也比较简单。比较重要的一点就是,如果当前有同步优先级的更新(syncLane),那么会立刻同步的执行flushPendingEffects方法。里面会同步的执行flushMutationEffectsflushLayoutEffectsflushPassiveEffects

最值得注意的就是会同步执行flushPassiveEffects了。这部分是处理passive effect的,即useEffect里面的内容。通常情况下,useEffect是在commit阶段结束后,下一个宏任务里去执行的,这个在上一篇commitRoot方法的源码里我们能够看到。但是当pendingEffectsLanes包含了同步优先级(在commitRoot里给pendingEffectsLanes赋值),则会改为同步执行。

值得注意的是,React项目初次渲染,组件mount的时候,优先级是DefaultLane,是低于同步优先级的。所以第一次的useEffect会异步执行。而如果是点击事件触发了状态更新,进而产生了passive effect,那么这次useEffect即passive effect,会同步执行。这对应这个解释useEffect fires synchronously when it’s the result of a discrete input.

接下来,React会检测处理副作用时候产生的新的更新,是否有导致无限的循环渲染。

最后,flushSyncWorkAcrossRoots_impl去把当前root上的所有同步的更新,执行一遍。也就是说,这里会直接同步地去重新渲染。即,如果useLayoutEffect里面有更新,那么React会在commit阶段的末尾,同步重新进入一遍render阶段

flushPassiveEffects

commitRoot里面的关键方法还剩flushPassiveEffects没讲。上面提到过,这个是执行useEffect里面的副作用的代码。

它的主要逻辑在flushPassiveEffectsImpl里,并且可以被简要提炼为下面几行

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
function flushPassiveEffectsImpl(wasDelayedCommit: void | boolean) {
// Cache and clear the transitions flag
const transitions = pendingPassiveTransitions;
pendingPassiveTransitions = null;

const root = pendingEffectsRoot;
const lanes = pendingEffectsLanes;
pendingEffectsStatus = NO_PENDING_EFFECTS;
pendingEffectsRoot = (null: any); // Clear for GC purposes.
pendingFinishedWork = (null: any); // Clear for GC purposes.
pendingEffectsLanes = NoLanes;

commitPassiveUnmountEffects(root.current);
commitPassiveMountEffects(
root,
root.current,
lanes,
transitions,
pendingEffectsRenderEndTime,
);

flushSyncWorkOnAllRoots();

return true;
}

首先commitPassiveUnmountEffects,这个前面看了mutation阶段和layout阶段,我们其实都能猜到了,从根节点开始,递归遍历全部子节点,把每个子节点上面的passive effect的return方法都执行一遍,且是先执行子节点的,再行父节点的。

commitPassiveMountEffects方法,则是递归遍历所有子节点,把他们的passive effect的内容都执行一遍。

这样我们其实也能看出来,useEffect的执行顺序,先是把所有Fiber上面的return按先子后父的顺序执行完,然后再按先子后父的顺序把原本的副作用执行一遍。

最后,再执行flushSyncWorkOnAllRoots方法,把所有的同步任务都清一遍。

结语

以上就是commitRoot阶段,React的主要流程。后续我们会了解一下Scheduler的工作逻辑。


通俗易懂的React原理(九):commit流程(下)
https://miku03090831.github.io/2025/10/03/通俗易懂的React原理(九):commit流程(下)/
作者
qh_meng
发布于
2025年10月3日
许可协议