文章主要解释了 React 如何处理状态更新,特别是其双缓冲机制 和优先级调度。
文章内容结合 deepseek 进行了汇总
一、UpdateQueue 的结构与双缓冲机制
1. 更新队列是一个链表
- 每个 Update 代表一个状态更新(如 setState)。
- 队列按插入顺序存储更新,而非按优先级排序。
2. 双队列设计:current 与 work-in-progress
React 为每个组件维护两个队列:
- current queue:对应已提交到 DOM 的当前状态。
- work-in-progress queue:对应正在进行的渲染状态,可异步计算。
关键行为:
- 当开始一次新的渲染时,会克隆 current queue 作为 work-in-progress queue 的初始值。
- 当提交(commit)时,work-in-progress queue 会成为新的 current queue。
- 如果渲染被中断并丢弃,则基于 current queue 重新创建 work-in-progress queue。
3. 为什么更新要同时追加到两个队列?
-
如果只追加到 work-in-progress queue,当渲染被丢弃并重新克隆 current queue 时,新更新会丢失。
-
如果只追加到 current queue,当 work-in-progress queue 提交并覆盖 current queue 时,新更新也会丢失。
-
同时追加到两个队列保证更新一定会被下一次渲染处理,且不会重复应用。
二、优先级处理机制
1. 更新按插入顺序存储,但按优先级处理
更新在链表中按插入顺序排列,不按优先级排序。
处理队列时,只处理优先级足够的更新。
如果某个更新因优先级不足被跳过,它之后的所有更新都会保留,即使它们优先级足够。
2. base state 的作用
-
base state 是队列中第一个更新之前的状态。
-
当跳过某些更新时,后续的高优先级更新会基于新的 base state 重新计算(rebase)。
3. 举例说明(注释中的例子):
初始状态:''
更新队列(字母为更新内容,数字为优先级):
A1 - B2 - C1 - D2
第一次渲染(优先级 1):
-
base state: ''
-
可处理的更新:A1(优先级1)、C1(优先级1)
-
跳过 B2(优先级不足)
-
结果状态:'AC'
-
注意:C1 是基于 'A' 状态处理的,但 B2 被跳过,所以 B2 和 D2 留在队列中。
第二次渲染(优先级 2):
-
base state: 'A'(跳过的 B2 之前的状态)
-
队列中剩余更新:B2、C1、D2
-
可处理的更新:B2、C1、D2(优先级都满足)
-
结果状态:'ABCD'
关键点:
- C1 被处理了两次(在两个优先级下),但最终状态是一致的。
- 最终结果与按顺序同步处理所有更新相同('ABCD')。
三、设计哲学与优势
1. 确定性最终状态
- 无论更新优先级如何、中间是否被中断,最终状态总是与同步顺序处理所有更新一致。
- 这是 React 可预测性的核心保证。
2. 时间切片与并发渲染的支撑
- 允许高优先级更新打断低优先级渲染。
- 被跳过的更新和后续更新会被保留,在更低优先级时处理。
3. 性能与响应性平衡
- 高优先级更新(如用户输入)可快速响应。
- 低优先级更新(如数据拉取)可等待或被打断。
四、实际应用中的体现
这种机制在 React 特性中体现为:
- 自动批处理:多个 setState 合并为一个更新。
- 并发特性(Concurrent Mode):高优先级更新可打断低优先级渲染。
- Suspense:数据获取更新可被推迟。
五、简单总结
| 特点 | 说明 |
|---|---|
| 双缓冲队列 | current(已提交)和 work-in-progress(计算中),保证更新不丢失。 |
| 插入顺序存储 | 更新按调用顺序追加到链表尾部,不按优先级排序。 |
| 优先级筛选处理 | 处理时跳过优先级不足的更新,但保留后续所有更新。 |
| base state 重定基 | 跳过更新时,后续更新基于新的 base state 重新计算。。 |
| 确定性结果 | 最终状态与同步处理所有更新一致,中间状态可能不同。。 |
这种设计让 React 在异步、可中断的渲染过程中,既能保证最终状态的一致性,又能实现优先级调度,优化用户体验。
附 ReactUpdateQueue.js 代码如下:
js
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// UpdateQueue is a linked list of prioritized updates.
//
// Like fibers, update queues come in pairs: a current queue, which represents
// the visible state of the screen, and a work-in-progress queue, which is
// can be mutated and processed asynchronously before it is committed --- a form
// of double buffering. If a work-in-progress render is discarded before
// finishing, we create a new work-in-progress by cloning the current queue.
//
// Both queues share a persistent, singly-linked list structure. To schedule an
// update, we append it to the end of both queues. Each queue maintains a
// pointer to first update in the persistent list that hasn't been processed.
// The work-in-progress pointer always has a position equal to or greater than
// the current queue, since we always work on that one. The current queue's
// pointer is only updated during the commit phase, when we swap in the
// work-in-progress.
//
// For example:
//
// Current pointer: A - B - C - D - E - F
// Work-in-progress pointer: D - E - F
// ^
// The work-in-progress queue has
// processed more updates than current.
//
// The reason we append to both queues is because otherwise we might drop
// updates without ever processing them. For example, if we only add updates to
// the work-in-progress queue, some updates could be lost whenever a work-in
// -progress render restarts by cloning from current. Similarly, if we only add
// updates to the current queue, the updates will be lost whenever an already
// in-progress queue commits and swaps with the current queue. However, by
// adding to both queues, we guarantee that the update will be part of the next
// work-in-progress. (And because the work-in-progress queue becomes the
// current queue once it commits, there's no danger of applying the same
// update twice.)
//
// Prioritization
// --------------
//
// Updates are not sorted by priority, but by insertion; new updates are always
// appended to the end of the list.
//
// The priority is still important, though. When processing the update queue
// during the render phase, only the updates with sufficient priority are
// included in the result. If we skip an update because it has insufficient
// priority, it remains in the queue to be processed later, during a lower
// priority render. Crucially, all updates subsequent to a skipped update also
// remain in the queue *regardless of their priority*. That means high priority
// updates are sometimes processed twice, at two separate priorities. We also
// keep track of a base state, that represents the state before the first
// update in the queue is applied.
//
// For example:
//
// Given a base state of '', and the following queue of updates
//
// A1 - B2 - C1 - D2
//
// where the number indicates the priority, and the update is applied to the
// previous state by appending a letter, React will process these updates as
// two separate renders, one per distinct priority level:
//
// First render, at priority 1:
// Base state: ''
// Updates: [A1, C1]
// Result state: 'AC'
//
// Second render, at priority 2:
// Base state: 'A' <- The base state does not include C1,
// because B2 was skipped.
// Updates: [B2, C1, D2] <- C1 was rebased on top of B2
// Result state: 'ABCD'
//
// Because we process updates in insertion order, and rebase high priority
// updates when preceding updates are skipped, the final result is deterministic
// regardless of priority. Intermediate state may vary according to system
// resources, but the final state is always the same.
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import {NoWork} from './ReactFiberExpirationTime';
import {Callback, ShouldCapture, DidCapture} from 'shared/ReactSideEffectTags';
import {ClassComponent} from 'shared/ReactWorkTags';
import {
debugRenderPhaseSideEffects,
debugRenderPhaseSideEffectsForStrictMode,
} from 'shared/ReactFeatureFlags';
import {StrictMode} from './ReactTypeOfMode';
import invariant from 'shared/invariant';
import warningWithoutStack from 'shared/warningWithoutStack';
export type Update<State> = {
expirationTime: ExpirationTime,
tag: 0 | 1 | 2 | 3,
payload: any,
callback: (() => mixed) | null,
next: Update<State> | null,
nextEffect: Update<State> | null,
};
export type UpdateQueue<State> = {
baseState: State,
firstUpdate: Update<State> | null,
lastUpdate: Update<State> | null,
firstCapturedUpdate: Update<State> | null, // ErrorBoundary 异常捕获使用
lastCapturedUpdate: Update<State> | null,
firstEffect: Update<State> | null,
lastEffect: Update<State> | null,
firstCapturedEffect: Update<State> | null,
lastCapturedEffect: Update<State> | null,
};
export const UpdateState = 0;
export const ReplaceState = 1;
export const ForceUpdate = 2;
export const CaptureUpdate = 3;
// Global state that is reset at the beginning of calling `processUpdateQueue`.
// It should only be read right after calling `processUpdateQueue`, via
// `checkHasForceUpdateAfterProcessing`.
let hasForceUpdate = false;
let didWarnUpdateInsideUpdate;
let currentlyProcessingQueue;
export let resetCurrentlyProcessingQueue;
if (__DEV__) {
didWarnUpdateInsideUpdate = false;
currentlyProcessingQueue = null;
resetCurrentlyProcessingQueue = () => {
currentlyProcessingQueue = null;
};
}
export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
baseState,
firstUpdate: null,
lastUpdate: null,
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}
function cloneUpdateQueue<State>(
currentQueue: UpdateQueue<State>,
): UpdateQueue<State> {
const queue: UpdateQueue<State> = {
baseState: currentQueue.baseState,
firstUpdate: currentQueue.firstUpdate,
lastUpdate: currentQueue.lastUpdate,
// TODO: With resuming, if we bail out and resuse the child tree, we should
// keep these effects.
firstCapturedUpdate: null,
lastCapturedUpdate: null,
firstEffect: null,
lastEffect: null,
firstCapturedEffect: null,
lastCapturedEffect: null,
};
return queue;
}
export function createUpdate(expirationTime: ExpirationTime): Update<*> {
return {
expirationTime: expirationTime,
tag: UpdateState,
payload: null,
callback: null,
next: null,
nextEffect: null,
};
}
function appendUpdateToQueue<State>(
queue: UpdateQueue<State>,
update: Update<State>,
) {
// Append the update to the end of the list.
if (queue.lastUpdate === null) {
// Queue is empty
queue.firstUpdate = queue.lastUpdate = update;
} else {
queue.lastUpdate.next = update;
queue.lastUpdate = update;
}
}
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
// Update queues are created lazily.
const alternate = fiber.alternate;
let queue1;
let queue2;
if (alternate === null) {
// There's only one fiber.
queue1 = fiber.updateQueue;
queue2 = null;
if (queue1 === null) {
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
}
} else {
// There are two owners.
queue1 = fiber.updateQueue;
queue2 = alternate.updateQueue;
if (queue1 === null) {
if (queue2 === null) {
// Neither fiber has an update queue. Create new ones.
queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
queue2 = alternate.updateQueue = createUpdateQueue(
alternate.memoizedState,
);
} else {
// Only one fiber has an update queue. Clone to create a new one.
queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
}
} else {
if (queue2 === null) {
// Only one fiber has an update queue. Clone to create a new one.
queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
} else {
// Both owners have an update queue.
}
}
}
if (queue2 === null || queue1 === queue2) {
// There's only a single queue.
appendUpdateToQueue(queue1, update);
} else {
// There are two queues. We need to append the update to both queues,
// while accounting for the persistent structure of the list --- we don't
// want the same update to be added multiple times.
if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
// One of the queues is not empty. We must add the update to both queues.
appendUpdateToQueue(queue1, update);
appendUpdateToQueue(queue2, update);
} else {
// Both queues are non-empty. The last update is the same in both lists,
// because of structural sharing. So, only append to one of the lists.
appendUpdateToQueue(queue1, update);
// But we still need to update the `lastUpdate` pointer of queue2.
queue2.lastUpdate = update;
}
}
if (__DEV__) {
if (
fiber.tag === ClassComponent &&
(currentlyProcessingQueue === queue1 ||
(queue2 !== null && currentlyProcessingQueue === queue2)) &&
!didWarnUpdateInsideUpdate
) {
warningWithoutStack(
false,
'An update (setState, replaceState, or forceUpdate) was scheduled ' +
'from inside an update function. Update functions should be pure, ' +
'with zero side-effects. Consider using componentDidUpdate or a ' +
'callback.',
);
didWarnUpdateInsideUpdate = true;
}
}
}
export function enqueueCapturedUpdate<State>(
workInProgress: Fiber,
update: Update<State>,
) {
// Captured updates go into a separate list, and only on the work-in-
// progress queue.
let workInProgressQueue = workInProgress.updateQueue;
if (workInProgressQueue === null) {
workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
workInProgress.memoizedState,
);
} else {
// TODO: I put this here rather than createWorkInProgress so that we don't
// clone the queue unnecessarily. There's probably a better way to
// structure this.
workInProgressQueue = ensureWorkInProgressQueueIsAClone(
workInProgress,
workInProgressQueue,
);
}
// Append the update to the end of the list.
if (workInProgressQueue.lastCapturedUpdate === null) {
// This is the first render phase update
workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
} else {
workInProgressQueue.lastCapturedUpdate.next = update;
workInProgressQueue.lastCapturedUpdate = update;
}
}
function ensureWorkInProgressQueueIsAClone<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
): UpdateQueue<State> {
const current = workInProgress.alternate;
if (current !== null) {
// If the work-in-progress queue is equal to the current queue,
// we need to clone it first.
if (queue === current.updateQueue) {
queue = workInProgress.updateQueue = cloneUpdateQueue(queue);
}
}
return queue;
}
function getStateFromUpdate<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
update: Update<State>,
prevState: State,
nextProps: any,
instance: any,
): any {
switch (update.tag) {
case ReplaceState: {
const payload = update.payload;
if (typeof payload === 'function') {
// Updater function
if (__DEV__) {
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
payload.call(instance, prevState, nextProps);
}
}
return payload.call(instance, prevState, nextProps);
}
// State object
return payload;
}
case CaptureUpdate: {
workInProgress.effectTag =
(workInProgress.effectTag & ~ShouldCapture) | DidCapture;
}
// Intentional fallthrough
case UpdateState: {
const payload = update.payload;
let partialState;
if (typeof payload === 'function') {
// Updater function
if (__DEV__) {
if (
debugRenderPhaseSideEffects ||
(debugRenderPhaseSideEffectsForStrictMode &&
workInProgress.mode & StrictMode)
) {
payload.call(instance, prevState, nextProps);
}
}
partialState = payload.call(instance, prevState, nextProps);
} else {
// Partial state object
partialState = payload;
}
if (partialState === null || partialState === undefined) {
// Null and undefined are treated as no-ops.
return prevState;
}
// Merge the partial state and the previous state.
return Object.assign({}, prevState, partialState);
}
case ForceUpdate: {
hasForceUpdate = true;
return prevState;
}
}
return prevState;
}
export function processUpdateQueue<State>(
workInProgress: Fiber,
queue: UpdateQueue<State>,
props: any,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
hasForceUpdate = false;
queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
if (__DEV__) {
currentlyProcessingQueue = queue;
}
// These values may change as we process the queue.
let newBaseState = queue.baseState;
let newFirstUpdate = null;
let newExpirationTime = NoWork;
// Iterate through the list of updates to compute the result.
let update = queue.firstUpdate;
let resultState = newBaseState;
while (update !== null) {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime > renderExpirationTime) {
// This update does not have sufficient priority. Skip it.
if (newFirstUpdate === null) {
// This is the first skipped update. It will be the first update in
// the new list.
newFirstUpdate = update;
// Since this is the first update that was skipped, the current result
// is the new base state.
newBaseState = resultState;
}
// Since this update will remain in the list, update the remaining
// expiration time.
if (
newExpirationTime === NoWork ||
newExpirationTime > updateExpirationTime
) {
newExpirationTime = updateExpirationTime;
}
} else {
// This update does have sufficient priority. Process it and compute
// a new result.
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
// Set this to null, in case it was mutated during an aborted render.
update.nextEffect = null;
if (queue.lastEffect === null) {
queue.firstEffect = queue.lastEffect = update;
} else {
queue.lastEffect.nextEffect = update;
queue.lastEffect = update;
}
}
}
// Continue to the next update.
update = update.next;
}
// Separately, iterate though the list of captured updates.
let newFirstCapturedUpdate = null;
update = queue.firstCapturedUpdate;
while (update !== null) {
const updateExpirationTime = update.expirationTime;
if (updateExpirationTime > renderExpirationTime) {
// This update does not have sufficient priority. Skip it.
if (newFirstCapturedUpdate === null) {
// This is the first skipped captured update. It will be the first
// update in the new list.
newFirstCapturedUpdate = update;
// If this is the first update that was skipped, the current result is
// the new base state.
if (newFirstUpdate === null) {
newBaseState = resultState;
}
}
// Since this update will remain in the list, update the remaining
// expiration time.
if (
newExpirationTime === NoWork ||
newExpirationTime > updateExpirationTime
) {
newExpirationTime = updateExpirationTime;
}
} else {
// This update does have sufficient priority. Process it and compute
// a new result.
resultState = getStateFromUpdate(
workInProgress,
queue,
update,
resultState,
props,
instance,
);
const callback = update.callback;
if (callback !== null) {
workInProgress.effectTag |= Callback;
// Set this to null, in case it was mutated during an aborted render.
update.nextEffect = null;
if (queue.lastCapturedEffect === null) {
queue.firstCapturedEffect = queue.lastCapturedEffect = update;
} else {
queue.lastCapturedEffect.nextEffect = update;
queue.lastCapturedEffect = update;
}
}
}
update = update.next;
}
if (newFirstUpdate === null) {
queue.lastUpdate = null;
}
if (newFirstCapturedUpdate === null) {
queue.lastCapturedUpdate = null;
} else {
workInProgress.effectTag |= Callback;
}
if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
// We processed every update, without skipping. That means the new base
// state is the same as the result state.
newBaseState = resultState;
}
queue.baseState = newBaseState;
queue.firstUpdate = newFirstUpdate;
queue.firstCapturedUpdate = newFirstCapturedUpdate;
// Set the remaining expiration time to be whatever is remaining in the queue.
// This should be fine because the only two other things that contribute to
// expiration time are props and context. We're already in the middle of the
// begin phase by the time we start processing the queue, so we've already
// dealt with the props. Context in components that specify
// shouldComponentUpdate is tricky; but we'll have to account for
// that regardless.
workInProgress.expirationTime = newExpirationTime;
workInProgress.memoizedState = resultState;
if (__DEV__) {
currentlyProcessingQueue = null;
}
}
function callCallback(callback, context) {
invariant(
typeof callback === 'function',
'Invalid argument passed as callback. Expected a function. Instead ' +
'received: %s',
callback,
);
callback.call(context);
}
export function resetHasForceUpdateBeforeProcessing() {
hasForceUpdate = false;
}
export function checkHasForceUpdateAfterProcessing(): boolean {
return hasForceUpdate;
}
export function commitUpdateQueue<State>(
finishedWork: Fiber,
finishedQueue: UpdateQueue<State>,
instance: any,
renderExpirationTime: ExpirationTime,
): void {
// If the finished render included captured updates, and there are still
// lower priority updates left over, we need to keep the captured updates
// in the queue so that they are rebased and not dropped once we process the
// queue again at the lower priority.
if (finishedQueue.firstCapturedUpdate !== null) {
// Join the captured update list to the end of the normal list.
if (finishedQueue.lastUpdate !== null) {
finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
}
// Clear the list of captured updates.
finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
}
// Commit the effects
commitUpdateEffects(finishedQueue.firstEffect, instance);
finishedQueue.firstEffect = finishedQueue.lastEffect = null;
commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);
finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
}
function commitUpdateEffects<State>(
effect: Update<State> | null,
instance: any,
): void {
while (effect !== null) {
const callback = effect.callback;
if (callback !== null) {
effect.callback = null;
callCallback(callback, instance);
}
effect = effect.nextEffect;
}
}
至此,结束。