04|从 Lane 位图到 getNextLanes:React 更新优先级与纠缠(Entangle)模型
本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到核心算法的关键设计。开源仓库:https://github.com/facebook/react
引言:为什么 WorkLoop 之外,还需要一整套 Lane 体系?
在第 3 篇我们把"更新进入引擎后如何被调度、渲染、提交"跑通了。但如果你回看那条链路,会发现 WorkLoop 的很多关键判断都外包给了 Lane:
scheduleUpdateOnFiber只是"把某个 lane 标记到 root 上"ensureRootIsScheduled在 microtask 中调用getNextLanes决定下一次渲染哪些 lanesperformWorkOnRoot用includesBlockingLane/includesExpiredLane决定是否 time-slicegetEntangledLanes影响 render 阶段实际使用的 lanes(entangledRenderLanes)
所以这一篇的目标是:
- 把 Lane 的位图模型讲清楚:它不是"优先级枚举",而是一套可组合、可纠缠、可追踪的调度数据结构
- 把
getNextLanes的选择策略讲清楚:为什么 pending/suspended/pinged/warm 这么多集合 - 把"纠缠 entangle"的语义讲清楚:为什么 Transition 需要"必须同批渲染"的约束
- 把"饥饿/过期"的安全阀讲清楚:React 如何避免低优先级更新永远跑不到
本文引用的主要源码文件:
packages/react-reconciler/src/ReactFiberLane.jspackages/react-reconciler/src/ReactFiberRootScheduler.jspackages/react-reconciler/src/ReactFiberHooks.jspackages/react-reconciler/src/ReactFiberClassUpdateQueue.jspackages/react-reconciler/src/ReactFiberConcurrentUpdates.jspackages/react/src/ReactStartTransition.jspackages/react/src/ReactTransitionType.js
1. Lane 是什么:31 位位图 + 可组合的"调度事实表"
1.1 Lane 常量:优先级不是数字,而是位的位置
在 ReactFiberLane.js 里,Lane 的核心定义是:
js
export type Lanes = number;
export type Lane = number;
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /* */ 0b0000000000000000000000000100000;
export const GestureLane: Lane = /* */ 0b0000000000000000000000001000000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111100000000;
const RetryLanes: Lanes = /* */ 0b0000011110000000000000000000000;
export const IdleLane: Lane = /* */ 0b0010000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b0100000000000000000000000000000;
export const DeferredLane: Lane = /* */ 0b1000000000000000000000000000000;
这段代码体现了 Lane 的几个关键特性:
- 一个 lane 是一个 bit ,天然可组合(
|)和可相交(&) - "优先级高低"不是
1/2/3,而是哪一位更靠右 (getHighestPriorityLane(lanes) = lanes & -lanes) - 不是所有 lanes 都是"更新 lanes"(例如 hydration / offscreen 等有特殊语义)
1.2 关键集合:SyncUpdateLanes / UpdateLanes
React 会经常用一组 lane 代表一个"语义集合",典型如:
js
export const SyncUpdateLanes: Lane =
SyncLane | InputContinuousLane | DefaultLane;
export const UpdateLanes: Lanes =
SyncLane | InputContinuousLane | DefaultLane | TransitionUpdateLanes;
注意:
UpdateLanes用于"是否算作一次更新"(例如无限循环检测、spawned deferred 纠缠时会过滤)- hydration lanes 被刻意排除在
UpdateLanes之外(它们更像"恢复/对齐 UI"而非应用更新)
2. Root 上的 lane 状态机:pending/suspended/pinged/warm/expired
Lane 的真正威力来自 FiberRoot 上的一组字段 。在 ReactFiberRoot.js:
js
this.pendingLanes = NoLanes;
this.suspendedLanes = NoLanes;
this.pingedLanes = NoLanes;
this.warmLanes = NoLanes;
this.expiredLanes = NoLanes;
this.entangledLanes = NoLanes;
this.entanglements = createLaneMap(NoLanes);
this.expirationTimes = createLaneMap(NoTimestamp);
你可以把它们理解成:
- pendingLanes:root 目前"还有活干"的 lanes
- suspendedLanes:这次渲染尝试中 suspend 过(因此暂时不能继续 render)的 lanes
- pingedLanes:曾 suspend 但数据回来了(wakeable resolved)的 lanes
- warmLanes:已经做过"预热 prewarm"(尝试渲染兄弟/后续内容)的 lanes
- expiredLanes:因为"饿太久/CPU-bound 太久"被判定必须尽快完成的 lanes
为什么要这么多集合?因为 React 的目标不是单纯"最高优先级先做",而是:
- 尽快完成可推进的工作(unblocked)
- 对 suspend 的工作保持记忆,并等待 ping
- 在没有 commit 压力时做 speculative prewarm
- 用 expiration 防止 starvation
这些全都落在 getNextLanes 的选择策略里。
3. getNextLanes:选择下一次 render lanes 的算法
文件:ReactFiberLane.js
3.1 第一层:非 Idle 永远优先于 Idle
js
const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
if (nonIdlePendingLanes !== NoLanes) {
const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
if (nonIdleUnblockedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdleUnblockedLanes);
} else {
const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
if (nonIdlePingedLanes !== NoLanes) {
nextLanes = getHighestPriorityLanes(nonIdlePingedLanes);
} else {
if (!rootHasPendingCommit) {
const lanesToPrewarm = nonIdlePendingLanes & ~warmLanes;
if (lanesToPrewarm !== NoLanes) {
nextLanes = getHighestPriorityLanes(lanesToPrewarm);
}
}
}
}
} else {
// The only remaining work is Idle.
// ...
}
这段代码有 3 条"优先级链":
- fresh updates(未 suspended)
- pinged updates(曾 suspended 但被唤醒)
- prewarm(在没有 pending commit 时做 speculative 预热)
这里的 rootHasPendingCommit 是重要的工程取舍:
- 如果已经有一棵"马上要 commit 的 finished tree",再去做 speculative 渲染通常不划算
- 先 commit 更能改善用户体验(让 UI 尽快稳定)
3.2 第二层:避免"低优先级打断高优先级的 in-progress render"
如果 root 正在渲染(wipLanes !== NoLanes),getNextLanes 会做一个"是否打断"的判断:
js
if (
wipLanes !== NoLanes &&
wipLanes !== nextLanes &&
(wipLanes & suspendedLanes) === NoLanes
) {
const nextLane = getHighestPriorityLane(nextLanes);
const wipLane = getHighestPriorityLane(wipLanes);
if (
nextLane >= wipLane ||
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
) {
return wipLanes;
}
}
这里有两个非常关键的行为:
- 只有更高优先级的 lanes 才能打断当前渲染(否则会丢进度)
- DefaultLane 不应该打断 TransitionLanes
- 源码注释指出:Default 与 Transition 的主要差别之一是 refresh transition 的语义
- 这是一种"体验语义优先于纯优先级"的规则
4. 饥饿与过期:markStarvedLanesAsExpired 的安全阀
ReactFiberRootScheduler 在 scheduleTaskForRootDuringMicrotask 里每次都会先调用:
js
markStarvedLanesAsExpired(root, currentTime);
其核心逻辑在 ReactFiberLane.js:
js
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = enableRetryLaneExpiration
? pendingLanes
: pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if ((lane & suspendedLanes) === NoLanes || (lane & pingedLanes) !== NoLanes) {
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
}
几个关键点:
- 只要一个 lane 没有 expirationTime,并且它不是"真正挂起的 suspended 未 ping",就会计算过期时间
- 如果到时间了,就把它并入
root.expiredLanes - 默认排除 retry lanes(除非
enableRetryLaneExpiration)- 源码说明:retry lanes 必须 time-slice,以便"unwrap uncached promises"
WorkLoop 侧会用 includesExpiredLane(root, lanes) 来决定是否禁用 time slicing:
js
export function includesExpiredLane(root: FiberRoot, lanes: Lanes): boolean {
return (lanes & root.expiredLanes) !== NoLanes;
}
这就是 React 的"兜底策略":
- 即便并发渲染允许让步
- 但如果某些更新被饿太久,必须更强硬地推进它
5. 纠缠(Entangle):Transition 为什么要"同批渲染"?
5.1 Root 级纠缠:entangledLanes + entanglements(LaneMap)
在 ReactFiberLane.js:
js
export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes) {
const rootEntangledLanes = (root.entangledLanes |= entangledLanes);
const entanglements = root.entanglements;
let lanes = rootEntangledLanes;
while (lanes) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
if ((lane & entangledLanes) | (entanglements[index] & entangledLanes)) {
entanglements[index] |= entangledLanes;
}
lanes &= ~lane;
}
}
这段代码的注释强调了一个容易被忽略的事实:
- 纠缠不是简单的 pairwise A<->B
- 它是传递闭包(transitive entanglement)
- "如果 C 已经与 A 纠缠,那么当 A 与 B 纠缠时,C 也必须与 B 纠缠"
5.2 Render 级纠缠:getEntangledLanes 影响 render 实际 lanes
在 ReactFiberWorkLoop.prepareFreshStack 中:
js
entangledRenderLanes = getEntangledLanes(root, lanes);
而 getEntangledLanes 做的事情是:
- 先以
renderLanes为基础 - 把 root 上与它纠缠的 lanes 全部并进来
js
export function getEntangledLanes(root: FiberRoot, renderLanes: Lanes): Lanes {
let entangledLanes = renderLanes;
const allEntangledLanes = root.entangledLanes;
if (allEntangledLanes !== NoLanes) {
const entanglements = root.entanglements;
let lanes = entangledLanes & allEntangledLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
entangledLanes |= entanglements[index];
lanes &= ~lane;
}
}
return entangledLanes;
}
直观理解:
- Scheduler 说"这次要做 lanes = X"
- WorkLoop 进入 render 前会把 "X 纠缠到的所有 lanes" 一起做
5.3 谁在调用 markRootEntangled?Hook 与 Class 两条入口都在
Hook:entangleTransitionUpdate
文件:ReactFiberHooks.js
js
function entangleTransitionUpdate<S, A>(
root: FiberRoot,
queue: UpdateQueue<S, A>,
lane: Lane,
): void {
if (isTransitionLane(lane)) {
let queueLanes = queue.lanes;
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
const newQueueLanes = mergeLanes(queueLanes, lane);
queue.lanes = newQueueLanes;
markRootEntangled(root, newQueueLanes);
}
}
调用链(同文件):
dispatchSetStateInternaldispatchReducerAction
都会在 scheduleUpdateOnFiber 之后调用 entangleTransitionUpdate(root, queue, lane)。
Class:entangleTransitions
文件:ReactFiberClassUpdateQueue.js
js
export function entangleTransitions(root: FiberRoot, fiber: Fiber, lane: Lane) {
const sharedQueue: SharedQueue<mixed> = (updateQueue: any).shared;
if (isTransitionLane(lane)) {
let queueLanes = sharedQueue.lanes;
queueLanes = intersectLanes(queueLanes, root.pendingLanes);
const newQueueLanes = mergeLanes(queueLanes, lane);
sharedQueue.lanes = newQueueLanes;
markRootEntangled(root, newQueueLanes);
}
}
你会发现 Hook 与 Class 的实现几乎一模一样:
- 它们都在自己的 queue 上维护一个"与该组件相关的 transition lanes 超集"
- 同时把这些 lanes 写入 root 的纠缠图里
为什么纠缠要放在 queue 上?
因为纠缠并不是"全局无条件"的:它通常与"某一条更新源(某个组件/某个 state queue)"强相关。
6. Transition lane 如何分配:同一事件尽量稳定,同一 async scope 必须共用
6.1 事件内稳定:requestTransitionLane 缓存 currentEventTransitionLane
文件:ReactFiberRootScheduler.js
js
let currentEventTransitionLane: Lane = NoLane;
export function requestTransitionLane(transition: Transition | null): Lane {
if (currentEventTransitionLane === NoLane) {
const actionScopeLane = peekEntangledActionLane();
currentEventTransitionLane =
actionScopeLane !== NoLane
? actionScopeLane
: claimNextTransitionUpdateLane();
}
return currentEventTransitionLane;
}
这段逻辑体现了"稳定性"的两个层次:
- 同一个事件内:Transition updates 共享同一个 transition lane
- 如果在 entangled async action scope 内 :必须复用
peekEntangledActionLane()(下一节)
6.2 Async action scope:entangleAsyncAction 让所有更新共享同一 lane
文件:ReactFiberAsyncAction.js
js
let currentEntangledLane: Lane = NoLane;
export function entangleAsyncAction<S>(
transition: Transition,
thenable: Thenable<S>,
): Thenable<S> {
if (currentEntangledListeners === null) {
currentEntangledLane = requestTransitionLane(transition);
// ...
}
currentEntangledPendingCount++;
thenable.then(pingEngtangledActionScope, pingEngtangledActionScope);
return thenable;
}
export function peekEntangledActionLane(): Lane {
return currentEntangledLane;
}
这段代码回答了一个现实问题:
- 浏览器现在没有"async context",React 无法精确知道一个更新属于哪一个 async action
- 所以 React 选择了一个工程折中:同一时间并发执行的 async actions 全部纠缠成一个 scope
- scope 内的所有 transition 更新共享同一 lane
而 Hooks / Class 的 update queue 在 render 时也会检测"是否读到了 entangled action lane",必要时直接 throw thenable 来 suspend:
-
ReactFiberHooks.updateReducerImpl:if (updateLane === peekEntangledActionLane()) didReadFromEntangledAsyncAction = true;- 最后
throw entangledActionThenable;
-
ReactFiberClassUpdateQueue.processUpdateQueue:- 同样会设置
didReadFromEntangledAsyncAction = true;
- 同样会设置
这就是"异步 action"与"Transition 批处理语义"之间的结合点。
7. 一图串起来:Lane 选择、过期、纠缠如何协同
不打断
打断/无 wip
更新产生
requestUpdateLane / requestTransitionLane
enqueueConcurrentHookUpdate / enqueueConcurrentClassUpdate
markUpdateLaneFromFiberToRoot: fiber.lanes and childLanes
markRootUpdated: root.pendingLanes assign-or lane
ensureRootIsScheduled
processRootScheduleInMicrotask
markStarvedLanesAsExpired
getNextLanes
是否打断 wip?
继续 wipLanes
选择 nextLanes
prepareFreshStack
getEntangledLanes: entangledRenderLanes
renderRootConcurrent OR renderRootSync
commitRoot
entangleTransitionUpdate / entangleTransitions
markRootEntangled + transitive closure
总结:你应该带走的 6 个结论
-
Lane 是位图,不是优先级枚举:它天然支持"同批次 lanes"与"集合运算"。
-
Root 上的 lanes 字段是调度事实表:pending/suspended/pinged/warm/expired 让 React 能在 suspend/恢复/speculative/兜底之间切换策略。
-
getNextLanes的核心不是"最高优先级",而是:
- non-idle 优先
- unblocked 优先
- pinged 次之
- 只有在没有 pending commit 压力时才做 prewarm
- 避免无意义打断 wip
-
过期机制是对并发让步的安全阀 :
markStarvedLanesAsExpired+includesExpiredLane让 React 在极端情况下更强硬地完成更新。 -
Entangle 是 Transition 语义的关键实现:并非为了"更快",而是为了"同源更新不展示中间态"。
-
Async action scope 是工程折中:没有 async context 时,React 用"纠缠 scope + 共享 lane + render 时必要时 suspend"来维持一致的过渡体验。
下一篇预告
第 5 篇我会专门把"并发渲染下的 Suspense 与 retry/ping"抽出来:
attachPingListener/pingSuspendedRootRootSuspendedWithDelay与 fallback throttle- retry lanes 如何与 commit/预热(prerender)机制协作