React Lane 模型
Lane 解决的是 React 更新系统里的“多优先级、多批次、多状态选择”问题。一次 setState 不只是把 update 放进 queue,还会给 update 分配一个 lane;这个 lane 会一路参与 root 标记、任务选择、render 阶段过滤 update、以及最终和 Scheduler priority 的衔接。
本专题位于 React 主链路的这段:
setState / dispatch
-> requestUpdateLane 选择 lane
-> update.lane 写入 updateQueue
-> scheduleUpdateOnFiber
-> markRootUpdated 更新 root.pendingLanes
-> ensureRootIsScheduled
-> getNextLanes 选择本轮 renderLanes
-> render 阶段用 renderLanes 消费 updateQueue
-> commit 后从 root.pendingLanes 移除已完成 lanes一、整体认知:Lane 是更新优先级和批次的位集合
Lane 可以先用一句话概括:
Lane 负责回答:
当前 root 上有哪些更新在等?
这次 render 应该处理哪些更新?
哪些更新因为优先级、Suspense 或隐藏树原因要留到后面?在 React 里,优先级不是只影响“先后顺序”。它还要同时表达几件事:
问题 | Lane 参与的答案 |
|---|---|
哪些更新已经产生? | 标记到 |
本轮 render 处理哪些更新? |
|
一个 update 本轮该不该计算? | 比较 |
一个 Suspense 更新是否被阻塞? | 记录到 |
Promise resolve 后哪些 lane 可重试? | 记录到 |
某些更新等太久怎么办? | 记录到 |
并发任务如何交给 Scheduler? |
|
所以 Lane 不是 Scheduler 的替代品。Lane 是 React reconciler 内部的更新模型;Scheduler 是更底层的任务调度器,负责把一个 callback 按优先级放到任务队列里,并在并发模式下配合时间切片让出主线程。
整体关系可以画成这样:
flowchart TD
A["用户事件 / setState"] --> B["requestUpdateLane"]
B --> C["update.lane"]
C --> D["updateQueue"]
C --> E["root.pendingLanes"]
E --> F["getNextLanes(root)"]
F --> G["renderLanes"]
G --> H["render 阶段过滤 updateQueue"]
F --> I["lanesToEventPriority"]
I --> J["SchedulerPriority"]
J --> K["scheduleCallback"]先建立这个总图很重要。后面看到 pendingLanes、renderLanes、eventPriority、schedulerPriority 时,不要把它们混成一个东西:它们是在不同层回答不同问题。
二、为什么需要 Lane:React 要同时支持可中断、可合并、可跳过、可恢复
早期把优先级理解成一个数字还勉强够用:数字越小越紧急,数字越大越不紧急。但并发更新里,React 面对的不是“当前只有一个任务”的场景,而是一个 root 上可能同时挂着很多类更新:
点击按钮产生同步更新
输入框产生高优先级更新
普通 setState 产生默认更新
startTransition 产生可中断更新
Suspense promise resolve 产生 retry 更新
隐藏 Offscreen 子树产生低优先级更新这些更新需要被合并到同一个 root 上,又要能在 render 时挑出一部分处理。更麻烦的是,低优先级更新不能因为被跳过就丢失;Suspense 阻塞后也不能一直卡住整个 root;一个 render 已经进行到一半时,如果来了更高优先级更新,还要判断是否要打断当前工作。
Lane 主要解决四类问题:
能力 | 说明 |
|---|---|
可合并 | 多次 update 可以通过按位或合并到同一个 |
可选择 |
|
可跳过 | render 阶段可以跳过不属于 |
可恢复 | Suspense ping、过期时间、warm lane 都可以继续影响后续选择 |
这也是面试里常见的表达:
Lane 不是单纯的优先级数字,而是 React 对“更新集合”的建模。
它让 root 能同时记录多类待处理更新,并在每轮 render 里选择、跳过、恢复、合并这些更新。理解了“为什么需要集合”,下一步就能解释为什么源码里 Lane 不是数组,而是 bitmask。
三、bitmask 心智模型:一个 bit 代表一条 lane
React 19 里 TotalLanes = 31。可以把 lanes 想象成一个 31 位的二进制集合:每一位代表一条 lane,这一位是 1 就表示集合里包含这条 lane。
源码里的几个常量:
const NoLanes = 0b0000000000000000000000000000000;
const SyncLane = 0b0000000000000000000000000000010; // 2
const InputContinuousLane = 0b0000000000000000000000000001000;
const DefaultLane = 0b0000000000000000000000000100000;
const TransitionLanes =
0b0000000001111111111111100000000;
const IdleLane =
0b0010000000000000000000000000000;SyncLane 为什么是 2?因为它不是第 0 位,而是第 1 位:
SyncHydrationLane = 0b...0001 = 1
SyncLane = 0b...0010 = 2bitmask 的好处是集合操作非常便宜:
操作 | 简化实现 | 含义 |
|---|---|---|
合并 |
| 把两个 lane 集合合成一个 |
判断包含 |
| 判断 set 里是否有 lane |
移除 |
| 从 set 里移除 lane |
取最高优先级 |
| 取最右侧的 |
示意一下多 lane 合并:
SyncLane 0b0000010
DefaultLane 0b0100000
merge 0b0100010
includes Sync? 0b0100010 & 0b0000010 = 0b0000010 => yes
remove Sync 0b0100010 & ~0b0000010 = 0b0100000
highest priority 0b0100010 & -0b0100010 = 0b0000010用图看就是:
flowchart LR
A["pendingLanes: Sync | Default | Transition"] --> B["getHighestPriorityLane"]
B --> C["SyncLane"]
A --> D["includesSomeLane(pendingLanes, DefaultLane)"]
D --> E["true"]
A --> F["removeLanes(pendingLanes, SyncLane)"]
F --> G["Default | Transition"]这里有一个重要细节:React Lane 的优先级大体是越靠右越高,也就是数值越小越高优先级。所以源码里比较 nextLane >= wipLane 时,含义是“next 的优先级不高于 wip”,因为更大的 bit 值通常更低优先级。
有了 bitmask 心智模型后,再看常见 Lane 类型就不会只是背名字了。
四、常见 Lane:从 Sync 到 Idle 的使用场景
Lane 类型很多,但面试和 debug 最常碰到的是这几组:
Lane | 典型值 / 集合 | 场景 | 特点 |
|---|---|---|---|
|
| 同步 hydration | 比普通 Sync 更特殊 |
|
| 离散事件、 | 最高常规更新优先级 |
|
| 滚动、拖拽、鼠标移动等连续输入 | 高于默认更新,低于离散点击 |
|
| 常规 | 普通更新 |
| 一组 bit |
| 可中断,可被更紧急更新抢占 |
| 一组 bit | Suspense retry | Promise resolve 后重试渲染 |
| 高位 bit | 空闲更新 | 非紧急,通常最后处理 |
| 高位 bit | 隐藏子树 / Offscreen 相关更新 | 常和隐藏树、预渲染有关 |
| 最高位 bit | deferred 相关工作 | 更偏延后处理 |
TransitionLanes 不是单条 lane,而是一段 lane 池。React 会从池子里循环分配 transition lane,这样多个 transition 可以在 root 上共存,也可以根据 entanglement 等关系被一起处理。
简化版:
let nextTransitionLane = TransitionLane1;
function requestTransitionLane() {
const lane = nextTransitionLane;
nextTransitionLane = getNextTransitionLane(nextTransitionLane);
return lane;
}从使用场景看,Lane 可以粗略分为两类:
紧急类:Sync / InputContinuous / Default
非紧急或可延后类:Transition / Retry / Idle / Offscreen / Deferred这个分类会影响后面的 getNextLanes:React 总是先看非 idle 的 pending work,再考虑 idle work;先看没有被 Suspense 阻塞的 work,再看 pinged work;必要时再做 warm / prewarm。
五、requestUpdateLane:为什么点击更新经常得到 SyncLane = 2
一次 Hook 更新的起点通常在 dispatchSetState:
function dispatchSetState(fiber, queue, action) {
const lane = requestUpdateLane(fiber);
const update = {
lane,
action,
next: null,
};
const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
scheduleUpdateOnFiber(root, fiber, lane);
}requestUpdateLane 的核心逻辑可以简化成这样:
function requestUpdateLane(fiber) {
if (!isConcurrentMode(fiber)) {
return SyncLane;
}
if (isRenderPhaseUpdate()) {
return pickArbitraryLane(workInProgressRootRenderLanes);
}
const transition = requestCurrentTransition();
if (transition !== null) {
return requestTransitionLane(transition);
}
const eventPriority = resolveUpdatePriority();
return eventPriorityToLane(eventPriority);
}所以它不是永远返回 2。常见返回路径是:
场景 | 返回 lane |
|---|---|
Legacy mode |
|
render phase update | 当前正在 render 的某个 lane |
| 某个 |
点击、键盘等离散事件 |
|
滚动、拖拽等连续事件 |
|
没有特殊事件上下文 |
|
idle 上下文 |
|
为什么点击按钮时经常看到 requestUpdateLane 得到 2?
click 是离散事件
-> ReactDOM 事件系统设置 currentUpdatePriority 为 DiscreteEventPriority
-> DiscreteEventPriority 在 ReactEventPriorities 中等于 SyncLane
-> eventPriorityToLane 直接返回这个 priority
-> SyncLane 的二进制是 0b...0010,十进制就是 2关键简化源码:
const DiscreteEventPriority = SyncLane;
const ContinuousEventPriority = InputContinuousLane;
const DefaultEventPriority = DefaultLane;
const IdleEventPriority = IdleLane;
function eventPriorityToLane(eventPriority) {
return eventPriority;
}再看 resolveUpdatePriority:
function resolveUpdatePriority() {
const currentPriority = getCurrentUpdatePriority();
if (currentPriority !== NoEventPriority) {
return currentPriority;
}
const event = window.event;
if (event === undefined) {
return DefaultEventPriority;
}
return getEventPriority(event.type);
}这段逻辑解释了一个很容易混淆的点:EventPriority 在类型上是事件优先级,但在实现上直接复用了 Lane 的 bit 值。也就是说,eventPriorityToLane 在 React 19 里看起来“什么都没做”,但它表达的是一层边界:事件系统给出事件优先级,reconciler 把它当作更新 lane 使用。
Lane 选出来以后,下一步就是把它标记到 root。
六、markRootUpdated 与 getNextLanes:root 如何记录并选择更新
每次 update 入队后,React 会沿 Fiber 找到 FiberRoot,然后把 lane 合并到 root 上:
function scheduleUpdateOnFiber(root, fiber, lane) {
markRootUpdated(root, lane);
ensureRootIsScheduled(root);
}markRootUpdated 的关键点:
function markRootUpdated(root, updateLane) {
root.pendingLanes |= updateLane;
if (updateLane !== IdleLane) {
root.suspendedLanes = NoLanes;
root.pingedLanes = NoLanes;
root.warmLanes = NoLanes;
}
}root.pendingLanes |= updateLane 是 Lane 模型的核心动作之一。它表示:
这棵 root 上现在有 updateLane 这类更新待处理。
如果之前还有别的 pending lane,就通过按位或保留在同一个集合里。例如:
第一次点击:pendingLanes = SyncLane
随后普通更新:pendingLanes = SyncLane | DefaultLane
随后 transition:pendingLanes = SyncLane | DefaultLane | TransitionLane1调度时,React 不会简单地把 pendingLanes 全部拿去 render,而是通过 getNextLanes 选择本轮 renderLanes。简化流程:
function getNextLanes(root, wipLanes, rootHasPendingCommit) {
const pending = root.pendingLanes;
if (pending === NoLanes) return NoLanes;
const suspended = root.suspendedLanes;
const pinged = root.pingedLanes;
const warm = root.warmLanes;
const nonIdlePending = pending & NonIdleLanes;
if (nonIdlePending !== NoLanes) {
const unblocked = nonIdlePending & ~suspended;
if (unblocked !== NoLanes) {
return getHighestPriorityLanes(unblocked);
}
const pingedLanes = nonIdlePending & pinged;
if (pingedLanes !== NoLanes) {
return getHighestPriorityLanes(pingedLanes);
}
if (!rootHasPendingCommit) {
const lanesToPrewarm = nonIdlePending & ~warm;
return getHighestPriorityLanes(lanesToPrewarm);
}
}
// 非 idle 都没有可做时,再考虑 idle pending。
return getHighestPriorityLanes(pending & ~suspended);
}实际源码还会处理更多细节,例如:
逻辑 | 作用 |
|---|---|
| root 没有待处理任务 |
优先处理 | idle work 不抢普通 work |
| 先选没被 Suspense 阻塞的更新 |
| Promise resolve 后可以重试被阻塞的更新 |
| 没有可提交任务时做 prewarm |
比较 | 决定是否打断正在进行的 render |
正在 render 时,React 还会避免不必要的中断。简化表达:
if (wipLanes !== NoLanes && wipLanes !== nextLanes) {
const nextLane = getHighestPriorityLane(nextLanes);
const wipLane = getHighestPriorityLane(wipLanes);
if (nextLane >= wipLane) {
return wipLanes; // 新任务没有更高优先级,继续当前 render
}
}这里的关键是:Lane 不只决定“谁先开始”,也决定“当前工作要不要被打断”。
讲到这里,root 上几个 lane 集合的含义就需要单独整理一下,否则 debug 时很容易只看到一串数字。
七、pending、suspended、pinged、expired、warm:root 上的 lane 状态表
FiberRoot 上不只保存 pendingLanes。React 会用多个 lane 集合记录同一批更新的不同状态:
字段 | 含义 | 什么时候变化 |
|---|---|---|
| root 上所有还没完成的更新集合 |
|
| render 时因为 Suspense 被阻塞的 lanes | 遇到 thenable / Suspense 时标记 |
| 被阻塞后,Promise resolve 了、可以重试的 lanes | thenable resolve 后标记 |
| 等太久,被认为饥饿的 lanes |
|
| 已经做过 prewarm / 预热尝试的 lanes | suspend / prewarm 路径中标记 |
| render 完成、等待 commit 的 lanes | render 完成后短暂使用 |
可以把一个 lane 的生命周期想成:
stateDiagram-v2
[*] --> Pending: markRootUpdated
Pending --> Rendering: getNextLanes selects
Rendering --> Suspended: throws thenable
Suspended --> Pinged: promise resolved
Pinged --> Rendering: getNextLanes selects again
Rendering --> Finished: render completed
Finished --> [*]: commit removes from pendingLanes
Pending --> Expired: waited too long
Suspended --> Warm: prewarm attempt几个字段的面试表达:
pendingLanes 表示还有哪些更新没完成。
suspendedLanes 表示哪些更新被 Suspense 阻塞,暂时不能直接继续。
pingedLanes 表示之前阻塞的更新已经被 Promise 唤醒,可以重新尝试。
expiredLanes 表示某些 lane 等太久了,React 要提升处理力度,避免饿死。
warmLanes 是 React 19 里和预热/预渲染相关的集合,用来避免对同一批 suspended lanes 重复做无意义 prewarm。warmLanes 可以稍微多解释一下。React 19 的 getNextLanes 在没有 fresh unblocked work、也没有 pinged work 时,会考虑对还没 warm 的 pending lanes 做 prewarm。它不是最终提交,而是尽量提前把可做的兄弟节点或异步路径预热一下,让后续真正可提交时更快。
这几个状态不是互斥的“枚举值”,而是多个 bitmask 集合。一个 lane 可以仍然在 pendingLanes 里,同时也出现在 suspendedLanes 或 pingedLanes 里。getNextLanes 的工作就是在这些集合之间做集合运算。
状态之外,还有一个和用户体验强相关的问题:低优先级任务一直被高优先级打断怎么办?
八、饥饿、过期时间与 updateQueue 匹配:既防饿死,也不丢更新
Lane 模型有两个容易被分开讲、但实际强相关的点:
root 层面:防止某些 lane 长期 pending,被高优先级任务饿死。
queue 层面:本轮没处理的 update 必须留在 baseQueue,后续还能重放。先看防饿死。Root Scheduler 每次调度时会检查有没有 lane 等太久:
function scheduleTaskForRootDuringMicrotask(root, currentTime) {
markStarvedLanesAsExpired(root, currentTime);
const nextLanes = getNextLanes(root, workInProgressRootRenderLanes, false);
// 后续根据 nextLanes 安排同步或并发任务
}markStarvedLanesAsExpired 简化后是:
function markStarvedLanesAsExpired(root, currentTime) {
let lanes = root.pendingLanes & ~RetryLanes;
while (lanes !== NoLanes) {
const lane = pickOneLane(lanes);
const expirationTime = root.expirationTimes[indexOf(lane)];
if (expirationTime === NoTimestamp) {
if (!isSuspended(lane) || isPinged(lane)) {
root.expirationTimes[indexOf(lane)] =
computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
lanes = removeLanes(lanes, lane);
}
}不同 lane 的过期策略不同:
Lane 类型 | 过期策略 |
|---|---|
| 较快过期,用户交互不能被长期饿住 |
| 有过期时间,但比同步交互更宽松 |
| 默认不一定过期,受特性开关影响 |
| 通常不设置过期时间 |
过期不等于 lane 的 bit 变了。它是把 lane 额外标记到 root.expiredLanes,让后续调度把它当作“不能再拖”的工作处理。
再看 updateQueue。每个 update 自己带着 lane;render 阶段消费 queue 时,会拿它和本轮的 renderLanes 比较。
Hook updateQueue 的简化逻辑:
function processUpdateQueue(hook, renderLanes) {
let newState = hook.baseState;
let newBaseQueue = null;
for (const update of hook.baseQueue) {
const updateLane = removeLanes(update.lane, OffscreenLane);
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 本轮优先级不够:跳过,但克隆到 baseQueue
newBaseQueue = appendClone(newBaseQueue, update);
continue;
}
// 本轮匹配:真正计算新 state
newState = reducer(newState, update.action);
}
hook.memoizedState = newState;
hook.baseQueue = newBaseQueue;
}这里的核心判断可以写成面试版:
如果 update.lane 属于 renderLanes,本轮就处理它。
如果不属于,本轮跳过,并把 update 保留到 baseQueue。
这样高优先级 render 可以先完成,低优先级 update 也不会丢。这就是 Lane 和 updateQueue 的关键配合:
层级 | 保存什么 | 解决什么 |
|---|---|---|
| root 级别还有哪些 lane 要做 | 调度和选择 renderLanes |
| 单个 update 属于哪个 lane | render 时判断该不该处理 |
| 被跳过的 update | 后续 render 重放,保证不丢 |
到这里,Lane 的内部模型已经完整了。最后还要把它和 Scheduler 的边界说清楚。
九、面试问答
1. Lane 是什么?
Lane 是 React reconciler 内部的更新优先级和批次模型。它用 bitmask 表示一组待处理更新,让 React 可以在 root 上合并多个更新,再根据优先级、Suspense 状态、过期时间等选择本轮要 render 的 lanes。
2. 为什么不用普通数字优先级?
普通数字只能表达“高低”,很难表达“集合”。React 需要同时记录多个待处理更新,还要做合并、包含、移除、选择最高优先级、跳过后保留等操作。bitmask 可以用非常便宜的位运算完成这些集合操作。
3. 为什么点击 setState 时 lane 经常是 2?
点击属于离散事件。ReactDOM 会把当前更新优先级设置成 DiscreteEventPriority,而 DiscreteEventPriority 在 React 19 中等于 SyncLane。SyncLane 的二进制是 0b...0010,十进制就是 2。
4. root.pendingLanes 是什么?
它是 FiberRoot 上所有待处理 lanes 的集合。每次 update 入队后,markRootUpdated(root, lane) 会执行 root.pendingLanes |= lane,把新的 lane 合并进去。commit 完成后,对应完成的 lanes 会从 pending 集合里移除。
5. getNextLanes 做了什么?
它从 root.pendingLanes 中选出本轮 render 应该处理的 renderLanes。选择时会优先非 idle work,排除 suspended work,考虑 pinged work,必要时考虑 warm / prewarm,并且会和当前正在 render 的 wipLanes 比较,决定是否打断当前工作。
6. suspended、pinged、expired、warm 分别是什么?
suspendedLanes 表示被 Suspense 阻塞的 lanes;pingedLanes 表示被阻塞后 Promise 已 resolve、可以重试的 lanes;expiredLanes 表示等太久需要防饿死的 lanes;warmLanes 表示已经做过预热尝试的 lanes,避免重复 prewarm。
7. Lane 如何保证低优先级 update 不丢?
每个 update 都带有自己的 update.lane。render 阶段处理 updateQueue 时,如果 update.lane 属于本轮 renderLanes,就计算它;如果不属于,就跳过并克隆到 baseQueue。后续低优先级 lane 被选中时,这些 update 会继续被重放。
8. Lane 和 Scheduler priority 的区别是什么?
Lane 是 React 内部用来描述更新集合和 render 选择的模型;Scheduler priority 是 scheduler 包用来安排 callback 执行顺序和时间切片的任务优先级。React 会先通过 Lane 选出 nextLanes,再把 lanes 转成 event priority,最后映射成 Scheduler priority。
9. TransitionLane 为什么可以被打断?
TransitionLane 表示非紧急 UI 更新。它仍然是 pending work,但当更高优先级的 Sync 或 InputContinuous 更新到来时,getNextLanes 可以选择更高优先级 lanes,当前 transition render 可能被中断。被跳过或未完成的 transition update 不会丢,会继续留在 root 和 queue 中等待后续处理。
10. 防止饿死靠什么?
React 会给部分 pending lanes 设置 expiration time。调度过程中 markStarvedLanesAsExpired 会检查等待时间,如果超过过期时间,就把 lane 加入 root.expiredLanes。这样低优先级任务不会无限期被更高优先级任务压住。


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