通俗易懂的React原理(六):一次状态更新如何触发渲染

代码以React v19.1.0为例,https://github.com/facebook/react/tree/v19.1.0

在本系列第二篇文章中,我们介绍了React是如何在一次渲染中,构建Fiber tree的。只不过我们当时是直接从workLoopConcurrentByScheduler/workLoopConcurrent/workLoopSync开始讲的,他们里面去循环遍历执行performUnitOfWork方法,构建Fiber tree。但是总归有点突兀,因为我们也不知道是谁调用的上面这三个方法。那么这一篇,我们就梳理一下调用流程,从一次状态更新,一直看到开始重新渲染。

一次同步更新

我们看个简单的例子,就比如

1
2
3
4
5
const [count, setCount] = useState(0);

const onClick = () => {
setCount(c=>c+1)
}

就比如这样一个简单的场景吧,当你点击了一个按钮,触发了onClick方法,那么我们知道执行setCount,会触发React的重新渲染。并且由于没有使用并发的API,所以这次重新渲染会是不可中断的,会执行到workLoopSync这个方法里。

我们就来看看setCount是最终如何走到workLoopSync的吧

setCount实际是dispatchSetState方法

前面我们介绍hooks的时候,以useReducer为例,讲了一下hooks在渲染过程中是怎么被处理的。useState其实是同理。在mount阶段,useState被替换成dispatchSetState方法,实际的重点逻辑在dispatchSetStateInternal里。

dispatchSetStateInternal的逻辑其实在番外篇一里,也简单看过啦,这里再看一遍吧

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
function dispatchSetStateInternal<S, A>(
fiber: Fiber,
queue: UpdateQueue<S, A>,
action: A,
lane: Lane,
): boolean {
const update: Update<S, A> = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: (null: any),
};

if (isRenderPhaseUpdate(fiber)) {
enqueueRenderPhaseUpdate(queue, update);
} else {
const alternate = fiber.alternate;
if (
fiber.lanes === NoLanes &&
(alternate === null || alternate.lanes === NoLanes)
) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
let prevDispatcher = null;
try {
const currentState: S = (queue.lastRenderedState: any);
const eagerState = lastRenderedReducer(currentState, action);

update.hasEagerState = true;
update.eagerState = eagerState;
if (is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
} catch (error) {
// Suppress the error. It will throw again in the render phase.
} finally {
if (__DEV__) {
ReactSharedInternals.H = prevDispatcher;
}
}
}
}

const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
entangleTransitionUpdate(root, queue, lane);
return true;
}
}
return false;
}

入参action就是我们传的更新行为,在我的例子里是c=>c+1这个函数

首先如果是渲染过程中的更新,那么会立刻把这个action加入到对应hook的queue.pending,这个环形链表当中。

如果不是渲染过程中的更新,那么则判断是否能做eagerState优化。要求是,当前Fiber和它的alernate节点,都不能有待处理的更新,并且执行action前后,state没有发生变化。如果满足条件,则命中eagerState优化,这一次不会重新渲染。

如果未命中优化,则正常将更新需要的参数都塞到数组里,然后执行scheduleUpdateOnFiber方法。

关键的scheduleUpdateOnFiber和ensureRootIsScheduled

scheduleUpdateOnFiber方法是很常用的,基本上看到它,就等同于安排重新渲染了。然而它里面还是判断了很多的逻辑,有很多分支我们都不会走进去。对于现阶段来讲,我们只需要关注它里面调用了ensureRootIsScheduled(root)即可

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
export function ensureRootIsScheduled(root: FiberRoot): void {
// This function is called whenever a root receives an update. It does two
// things 1) it ensures the root is in the root schedule, and 2) it ensures
// there's a pending microtask to process the root schedule.
//
// Most of the actual scheduling logic does not happen until
// `scheduleTaskForRootDuringMicrotask` runs.

// Add the root to the schedule
if (root === lastScheduledRoot || root.next !== null) {
// Fast path. This root is already scheduled.
} else {
if (lastScheduledRoot === null) {
firstScheduledRoot = lastScheduledRoot = root;
} else {
lastScheduledRoot.next = root;
lastScheduledRoot = root;
}
}

// Any time a root received an update, we set this to true until the next time
// we process the schedule. If it's false, then we can quickly exit flushSync
// without consulting the schedule.
mightHavePendingSyncWork = true;

if (!didScheduleMicrotask) {
didScheduleMicrotask = true;
scheduleImmediateRootScheduleTask();
}
}

ensureRootIsScheduled方法,负责将一个root加入调度。这里可以看到,React依然是用环形列表来维护需要调度的root的,React在频繁需要尾插的场景都是使用的环形列表。当然,通常来讲我们的应用里只有一个root。少数的应用,可能会在多处调用ReactDom.createRoot,这样就会有多个root。处于简单考虑,我们理解的时候可以只去想一个root的场景。

然后,将mightHavePendingSyncWork设为true,这里有什么用呢?todo

接下来,如果当前已经在调度中了,那么就不需要调度了,保证了一次事件循环里,只触发一次重新渲染。如果没有调度,则标记为正在调度,并且进行调度,即执行scheduleImmediateRootScheduleTask

scheduleImmediateRootScheduleTask方法源码如下

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
function scheduleImmediateRootScheduleTask() {
// TODO: Can we land supportsMicrotasks? Which environments don't support it?
// Alternatively, can we move this check to the host config?
if (supportsMicrotasks) {
scheduleMicrotask(() => {
// In Safari, appending an iframe forces microtasks to run.
// https://github.com/facebook/react/issues/22459
// We don't support running callbacks in the middle of render
// or commit so we need to check against that.
const executionContext = getExecutionContext();
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
// Note that this would still prematurely flush the callbacks
// if this happens outside render or commit phase (e.g. in an event).

// Intentionally using a macrotask instead of a microtask here. This is
// wrong semantically but it prevents an infinite loop. The bug is
// Safari's, not ours, so we just do our best to not crash even though
// the behavior isn't completely correct.
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
return;
}
processRootScheduleInMicrotask();
});
} else {
// If microtasks are not supported, use Scheduler.
Scheduler_scheduleCallback(
ImmediateSchedulerPriority,
processRootScheduleInImmediateTask,
);
}
}

scheduleImmediateRootScheduleTask的内容也很简单,判断一下当前宿主环境是否支持微任务。在react-dom中,supportsMicrotasks是true,所以不会走到最下面用scheduler调度的分支里去。那个分支最快会在下个宏任务开始渲染。而在支持微任务的场景下,最快会在下个微任务开始渲染。

微任务调度,会通过scheduleMicrotask来实现。scheduleMicrotask其实就是queueMicrotask。如果浏览器不支持queueMicrotaskAPI,则会通过promise去生成一个微任务。然后在微任务里面,去执行processRootScheduleInMicrotask.

不过如果当前是在render或者commit流程中,那么要使用宏任务去调度,这是为了避免无限循环。这种场景只在safari导致的bug中会出现。

如果是正常场景,则会调用processRootScheduleInMicrotask方法。这个方法的功能如字面意思所示,是在微任务中调度。在另外通过宏任务调度是,callback方法是processRootScheduleInImmediateTask,两个callback是不一样的。

processRootScheduleInMicrotask方法中,react会选出下一次渲染需要执行的优先级,最后开始重新渲染

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
function processRootScheduleInMicrotask() {
didScheduleMicrotask = false;

// We'll recompute this as we iterate through all the roots and schedule them.
mightHavePendingSyncWork = false;

let syncTransitionLanes = NoLanes;
const currentTime = now();

let prev = null;
let root = firstScheduledRoot;
while (root !== null) {
const next = root.next;
const nextLanes = scheduleTaskForRootDuringMicrotask(root, currentTime);
if (nextLanes === NoLane) {
// This root has no more pending work. Remove it from the schedule. To
// guard against subtle reentrancy bugs, this microtask is the only place
// we do this — you can add roots to the schedule whenever, but you can
// only remove them here.

// Null this out so we know it's been removed from the schedule.
root.next = null;
if (prev === null) {
// This is the new head of the list
firstScheduledRoot = next;
} else {
prev.next = next;
}
if (next === null) {
// This is the new tail of the list
lastScheduledRoot = prev;
}
} else {
// This root still has work. Keep it in the list.
prev = root;

// This is a fast-path optimization to early exit from
// flushSyncWorkOnAllRoots if we can be certain that there is no remaining
// synchronous work to perform. Set this to true if there might be sync
// work left.
if (
// Common case: we're not treating any extra lanes as synchronous, so we
// can just check if the next lanes are sync.
includesSyncLane(nextLanes)
) {
mightHavePendingSyncWork = true;
}
}
root = next;
}

// At the end of the microtask, flush any pending synchronous work. This has
// to come at the end, because it does actual rendering work that might throw.
flushSyncWorkAcrossRoots_impl(syncTransitionLanes, false);
}

首先,将didScheduleMicrotask置为false。这个是防止重复调度的,你既然已经开始执行了,那么相当于调度已经完成了,所以将它改为false。

然后将mightHavePendingSyncWork置为false,因为这个值会在下面重新计算,看root上面是否真的有同步任务。

接下来对遍历root,我们可以忽略循环,只看里面要执行的代码。因为大多数情况,我们的react应用只有一个root。要通过scheduleTaskForRootDuringMicrotask方法,找到这个root下面,下一次渲染要执行的优先级。如果是同步优先级,那么直接返回。如果是低优先级,则会通过scheduler去调度低优先级中,优先级最高的任务。

如果nextLanesNoLane了,就说明这个root没有待完成的更新了。

如果nextLanes有值,如果它是同步的优先级,那么将mightHavePendingSyncWork设置为true。

然后执行flushSyncWorkAcrossRoots_impl,开始渲染,把所有同步优先级的任务都执行干净。

因为这一次是同步的更新,所以我们只关注flushSyncWorkAcrossRoots_impl就可以了。


通俗易懂的React原理(六):一次状态更新如何触发渲染
https://miku03090831.github.io/2025/09/04/通俗易懂的React原理(六):一次状态更新如何触发渲染/
作者
qh_meng
发布于
2025年9月4日
许可协议