React Update Queue
UpdateQueue 解决的是一个核心问题:
setState / dispatch 发生时,React 不能立刻改 state、改 DOM。
它需要先把“更新意图”暂存起来,再在合适的 render 中按优先级和顺序统一计算。它和 Hook、Lane、render 的关系可以先这样看:
Hook 保存组件内部某一个状态槽位
UpdateQueue 保存这个 Hook 上还没被 render 消费的 update
Lane 标记每个 update 的优先级和所属批次
render 阶段根据 renderLanes 选择哪些 update 本轮应用、哪些跳过这篇笔记不再按断点流水账组织,而是围绕一次 Hook 更新真正经过的结构来学:
useState mount 创建 Hook 和 queue
-> dispatchSetState 创建 update
-> enqueueConcurrentHookUpdate 暂存到 concurrentQueues
-> finishQueueingConcurrentUpdates 并入 queue.pending 环状链表
-> scheduleUpdateOnFiber 调度 root
-> renderWithHooks 重新执行函数组件
-> updateReducerImpl 合并 pendingQueue 和 baseQueue
-> 根据 renderLanes 应用或跳过 update
-> 得到 memoizedState / baseState / baseQueue一、整体认知:UpdateQueue 是 React 的更新暂存区
一次 setState 不等于“直接把 state 改掉”。更准确地说,它表示:
创建一个 Update 对象
-> 放进对应 Hook 的 UpdateQueue
-> 标记 root 上有某个 lane 的工作
-> 等 render 阶段统一计算新 state所以 UpdateQueue 的价值不是“存一个新值”这么简单,而是让 React 同时处理三件事:
问题 | UpdateQueue 的作用 |
|---|---|
多次更新 | 按触发顺序排队,render 时统一计算 |
并发优先级 | 每个 update 带 lane,本轮只处理命中的 update |
跳过后恢复 | 被跳过的 update 进入 |
快速 bailout | 同值更新可通过 |
以最常见的函数组件为例:
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('Ada');
return <button => setCount(c => c + 1)}>{count}</button>;
}点击按钮时,setCount 触发的是 Hook queue 的入队和 root 调度。真正的 count + 1 计算,要等下一次 render 执行到这个 Hook 时才完成。
这也是理解本篇后续所有细节的入口:
dispatch 阶段负责“记录更新意图并调度”
render 阶段负责“消费更新队列并计算新 state”
commit 阶段负责“提交 render 的结果”二、Hook 结构:多个 useState 对应多个 Hook 节点和多个 queue
要理解 Hook UpdateQueue,先要明确 Hook 是什么。
在 React 内部,Hook 不是函数本身,也不是 [state, setState] 这个数组。Hook 是挂在 FunctionComponent Fiber 上的一个链表节点,用来保存某一次 Hook 调用对应的运行时状态。
简化结构:
const hook = {
memoizedState, // 本轮 render 后这个 Hook 的 state
baseState, // 重放 baseQueue 时的起点
baseQueue, // 被跳过、后续要继续处理的 update
queue, // 当前 Hook 自己的 UpdateQueue
next, // 下一个 Hook
};函数组件 Fiber 只通过 memoizedState 指向第一个 Hook:
flowchart LR
F["FunctionComponent Fiber"] --> H1["Hook 1: useState(count)"]
H1 --> H2["Hook 2: useState(name)"]
H2 --> H3["Hook 3: useEffect(...)"]
H3 --> N["null"]
H1 --> Q1["queue(count)"]
H2 --> Q2["queue(name)"]这张图很重要:同一个函数组件里的多个 useState 不会合成一个大 state,也不会共用一个队列。每一次 Hook 调用都有自己的 Hook 节点,useState(count) 和 useState(name) 各有独立的 queue。
因此 Hook 的调用顺序必须稳定。React 在 update 阶段不是靠变量名找 Hook,而是按组件函数执行时的 Hook 调用顺序,依次复用或克隆 Hook 节点。
简化版 mount:
function mountState(initialState) {
const hook = mountWorkInProgressHook();
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;
queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue);
return [hook.memoizedState, queue.dispatch];
}有了 Hook 和 queue 之后,setCount 才知道自己要把 update 放进哪一个队列。接下来就进入 dispatch 阶段。
三、dispatch 阶段:setState 是触发更新入口,不是 render / commit
dispatchSetState 发生在用户事件、异步回调、effect 等外部触发点里。它不是 render 阶段,也不是 commit 阶段。
这一步做的事情可以概括为:
选择 lane
-> 创建 Update
-> 尝试 eagerState 优化
-> 把 update 暂存到 concurrentQueues
-> 找到 root 并调度Hook update 对象的核心字段:
字段 | 含义 |
|---|---|
| 本次 update 的优先级和批次 |
| 乐观更新等场景用于回滚的 lane,普通 useState 可先忽略 |
| 传给 |
| 是否已经提前算出 eager state |
| dispatch 阶段提前计算出的下一个 state |
| 指向下一个 update,用来组成环状链表 |
简化版源码:
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
const update = {
lane,
revertLane: NoLane,
action,
hasEagerState: false,
eagerState: null,
next: null,
};
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
if (root !== null) {
scheduleUpdateOnFiber(root, fiber, lane);
}
}这里有一个容易混淆的点:
dispatchSetState 不负责重新执行组件。
它只是把 update 放进队列,并让 root 进入调度系统。
组件函数重新执行发生在后续 render 阶段的 renderWithHooks 中。
DOM 修改发生在更后面的 commit 阶段。面试表达:
setState 是更新入口,不是状态计算本身。React 会在 dispatch 阶段为这次更新选择 lane,
创建 update,并把 update 挂到对应 Hook 的 queue 上。随后 React 从触发更新的 Fiber
向上找到 FiberRoot,标记 pendingLanes 并调度后续 render。不过,React 并不一定在 dispatchSetState 里立刻把 update 接到 queue.pending。并发模式下,它还会经过一个临时暂存区,这就是 concurrentQueues。
四、入队结构:pending 环状链表与 concurrentQueues 暂存
Hook queue 的 pending 使用环状链表,并且 queue.pending 指向最后一个 update。
如果连续触发三次:
setCount(c => c + 1);
setCount(c => c + 1);
setCount(c => c + 1);队列结构不是普通数组,而是:
flowchart LR
P["queue.pending"] --> U3["update3: tail"]
U3 --> U1["update1: first"]
U1 --> U2["update2"]
U2 --> U3为什么 pending 指向尾节点?
插入新 update 只需要改两个 next 指针。
读取时又可以通过 pending.next 立刻拿到首节点。简化插入逻辑:
function appendUpdateToQueue(queue, update) {
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}但 React 19 的并发更新入口还多了一层 concurrentQueues。enqueueConcurrentHookUpdate 会先把四元组暂存到一个全局数组里:
concurrentQueues = [
fiber, queue, update, lane,
fiber, queue, update, lane,
...
]简化版源码:
function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
concurrentQueues[concurrentQueuesIndex++] = fiber;
concurrentQueues[concurrentQueuesIndex++] = queue;
concurrentQueues[concurrentQueuesIndex++] = update;
concurrentQueues[concurrentQueuesIndex++] = lane;
fiber.lanes = mergeLanes(fiber.lanes, lane);
return getRootForUpdatedFiber(fiber);
}之后 finishQueueingConcurrentUpdates 会统一把这些暂存 update 并入各自的 queue.pending:
function finishQueueingConcurrentUpdates() {
for (let i = 0; i < concurrentQueuesIndex; i += 4) {
const fiber = concurrentQueues[i];
const queue = concurrentQueues[i + 1];
const update = concurrentQueues[i + 2];
const lane = concurrentQueues[i + 3];
if (queue !== null && update !== null) {
appendUpdateToQueue(queue, update);
}
if (lane !== NoLane) {
markUpdateLaneFromFiberToRoot(fiber, lane);
}
}
concurrentQueuesIndex = 0;
}这一层暂存的意义是:事件或并发执行过程中可能产生多个 Hook 更新,React 可以先收集,再在安全边界统一写入各自队列,并把 lane 从当前 Fiber 冒泡到 root。
到这里,update 已经进入 queue.pending,root 也被标记了 lane。下一步才轮到 render 阶段消费这些 update。
五、render 消费:updateReducerImpl 如何计算新 state
函数组件更新时,React 会进入:
beginWork
-> updateFunctionComponent
-> renderWithHooks
-> Component(props)
-> useState / useReducer
-> updateReducerImpluseState 可以看作是 useReducer 的特例:
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}render 阶段处理 Hook queue 的核心不是“取最后一个 update”,而是从队列起点开始按顺序计算。因为函数式更新依赖前一个结果:
setCount(c => c + 1);
setCount(c => c + 1);最终应该是 +2,不是只看最后一次。
简化版消费流程:
function updateReducerImpl(hook, reducer) {
const queue = hook.queue;
let baseQueue = hook.baseQueue;
const pendingQueue = queue.pending;
if (pendingQueue !== null) {
queue.pending = null;
baseQueue = mergeQueues(baseQueue, pendingQueue);
}
let newState = hook.baseState;
let newBaseState = null;
let newBaseQueue = null;
let update = firstUpdate(baseQueue);
while (update !== null) {
if (!isSubsetOfLanes(renderLanes, update.lane)) {
newBaseQueue = cloneAndAppend(newBaseQueue, update);
if (newBaseState === null) {
newBaseState = newState;
}
} else {
newState = update.hasEagerState
? update.eagerState
: reducer(newState, update.action);
}
update = nextUpdate(update, baseQueue);
}
hook.memoizedState = newState;
hook.baseState = newBaseState === null ? newState : newBaseState;
hook.baseQueue = newBaseQueue;
queue.lastRenderedState = newState;
return [hook.memoizedState, queue.dispatch];
}这段逻辑有两个关键动作:
先把 queue.pending 合并进 baseQueue,形成本轮可遍历队列。
再按 renderLanes 决定每个 update 是应用,还是跳过并克隆到新的 baseQueue。也就是说,UpdateQueue 的完整生命周期是:
dispatch 阶段:update 进入 queue.pending
render 阶段:pendingQueue 合并到 baseQueue
render 阶段:命中 renderLanes 的 update 被应用
render 阶段:没命中的 update 被保留到新的 baseQueue接下来就要解释:为什么 React 需要 baseState 和 baseQueue,而不是简单地把跳过的 update 留在原地。
六、跳过与重放:baseState / baseQueue 保证低优先级更新不丢
Lane 决定本轮 render 处理哪些 update。
update.lane 命中 renderLanes -> 本轮应用,参与计算 newState
update.lane 不命中 renderLanes -> 本轮跳过,克隆到 baseQueue举一个简化例子:
初始 state = 0
update1: lane = DefaultLane, action = +1
update2: lane = TransitionLane, action = +10
update3: lane = DefaultLane, action = +1
本轮 renderLanes = DefaultLane本轮计算时:
update1 命中,应用:0 -> 1
update2 不命中,跳过,保存到 baseQueue
update3 命中,应用:1 -> 2但后续 TransitionLane 被处理时,React 不能只从当前 memoizedState = 2 开始简单加 +10。因为真实队列顺序里,update2 发生在 update3 之前。为了让重放仍然遵守原始更新顺序,React 会记录:
baseState = 第一个被跳过 update 之前已经算出的 state
baseQueue = 从被跳过 update 开始,需要后续重放的 update 链三个 state 相关字段的区别:
字段 | 含义 |
|---|---|
| 本轮 render 结束后,当前 Hook 展示出来的 state |
| 后续重放 |
| 本轮被跳过、以及跳过后为保证顺序需要重放的 update |
面试表达:
React 的 Hook queue 不是普通先进先出队列,它要服务并发渲染。
当某个 update 的 lane 不属于当前 renderLanes 时,React 会跳过它,
并把它保留到 baseQueue。baseState 记录从哪里重新计算,保证后续高低优先级
交错时,更新既不丢失,也不破坏原始顺序。这就把 UpdateQueue 和 Lane 模型连接起来了:UpdateQueue 保存“有哪些更新”,Lane 决定“本轮处理哪些更新”。
七、eagerState:dispatch 阶段的提前计算与 bailout
eagerState 是一个性能优化:在某些条件下,React 可以在 dispatch 阶段提前算出下一次 state。
典型场景:
setCount(0); // 当前 count 已经是 0如果当前 Fiber 和 alternate 上都没有其它待处理工作,React 可以直接用上一次 reducer 和当前 state 试算:
function dispatchSetStateInternal(fiber, queue, action, lane) {
const update = createUpdate(action, lane);
if (fiber.lanes === NoLanes && fiber.alternate?.lanes === NoLanes) {
const currentState = queue.lastRenderedState;
const eagerState = queue.lastRenderedReducer(currentState, action);
update.hasEagerState = true;
update.eagerState = eagerState;
if (Object.is(eagerState, currentState)) {
enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
return false;
}
}
enqueueConcurrentHookUpdate(fiber, queue, update, lane);
return true;
}它有两层作用:
如果 eagerState 和当前 state 相同,可以提前 bailout,避免一次不必要调度。
如果后续仍然进入 render,updateReducerImpl 可以直接使用 eagerState,少跑一次 reducer。不过不要把 eagerState 理解成“React 已经完成更新”。它只是 dispatch 阶段的提前试算。真正决定 UI 的仍然是后续 render 和 commit。
八、面试问答
常见面试问答:
1. Hook 是什么?
Hook 是挂在函数组件 Fiber 上的链表节点。每次 useState / useReducer 调用都会对应一个 Hook 节点,节点上保存 memoizedState、baseState、baseQueue、queue 等运行时信息。
2. 多个 useState 会共用一个 queue 吗?
不会。多个 useState 对应多个 Hook 节点,每个 Hook 节点有自己的 queue。React 通过 Hook 调用顺序在 update 阶段找到对应节点。
3. setState 后 React 立刻 render 吗?
不应这样理解。setState 的 dispatch 阶段会创建 update、选择 lane、入队并调度 root。真正重新执行组件发生在后续 render 阶段,DOM 修改发生在 commit 阶段。
4. 为什么 queue.pending 是环状链表?
因为它可以 O(1) 插入新 update,同时通过 pending.next 找到首个 update,适合多次更新按顺序合并和遍历。
5. concurrentQueues 是什么?
它是并发更新的临时暂存区。React 会先把 fiber / queue / update / lane 放到全局数组里,再由 finishQueueingConcurrentUpdates 统一并入各自的 queue.pending,并把 lane 冒泡到 root。
6. baseQueue 解决什么问题?
它保存本轮因为 lane 不匹配而跳过的 update,以及为了保持顺序需要后续重放的 update。配合 baseState,React 可以在之后处理低优先级 update 时恢复正确计算。
7. Lane 和 UpdateQueue 怎么配合?
UpdateQueue 保存更新对象,Lane 标记每个 update 的优先级。render 阶段用 renderLanes 判断 update 是否应该应用。不命中的 update 会被跳过并保留到 baseQueue。
8. Hook queue 和类组件 queue 有什么不同?
Hook queue 挂在每个 Hook 节点上,一个 Hook 一个 queue;类组件 queue 挂在 class component Fiber 上,服务整个实例 state。两者结构不同,但都要解决 update 排队、优先级选择、跳过后恢复这些问题。
一句话总结:
UpdateQueue 是 React 把“触发更新”和“计算状态”解耦的核心结构。
dispatch 阶段只负责把 update 带着 lane 入队并调度;
render 阶段再根据 renderLanes 消费队列,得到新的 state 和可继续重放的 baseQueue。

评论
还没有评论
来发表第一条评论吧!