04|从 Lane 位图到 `getNextLanes`:React 更新优先级与纠缠(Entangle)模型

04|从 Lane 位图到 getNextLanes:React 更新优先级与纠缠(Entangle)模型

本栏目是「React 源码剖析」系列:我会以源码为证据、以架构为线索,讲清 React 从构建、运行时到核心算法的关键设计。开源仓库:https://github.com/facebook/react

引言:为什么 WorkLoop 之外,还需要一整套 Lane 体系?

在第 3 篇我们把"更新进入引擎后如何被调度、渲染、提交"跑通了。但如果你回看那条链路,会发现 WorkLoop 的很多关键判断都外包给了 Lane:

  • scheduleUpdateOnFiber 只是"把某个 lane 标记到 root 上"
  • ensureRootIsScheduled 在 microtask 中调用 getNextLanes 决定下一次渲染哪些 lanes
  • performWorkOnRootincludesBlockingLane/includesExpiredLane 决定是否 time-slice
  • getEntangledLanes 影响 render 阶段实际使用的 lanes(entangledRenderLanes

所以这一篇的目标是:

  • 把 Lane 的位图模型讲清楚:它不是"优先级枚举",而是一套可组合、可纠缠、可追踪的调度数据结构
  • getNextLanes 的选择策略讲清楚:为什么 pending/suspended/pinged/warm 这么多集合
  • 把"纠缠 entangle"的语义讲清楚:为什么 Transition 需要"必须同批渲染"的约束
  • 把"饥饿/过期"的安全阀讲清楚:React 如何避免低优先级更新永远跑不到

本文引用的主要源码文件:

  • packages/react-reconciler/src/ReactFiberLane.js
  • packages/react-reconciler/src/ReactFiberRootScheduler.js
  • packages/react-reconciler/src/ReactFiberHooks.js
  • packages/react-reconciler/src/ReactFiberClassUpdateQueue.js
  • packages/react-reconciler/src/ReactFiberConcurrentUpdates.js
  • packages/react/src/ReactStartTransition.js
  • packages/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 条"优先级链":

  1. fresh updates(未 suspended)
  2. pinged updates(曾 suspended 但被唤醒)
  3. 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 的安全阀

ReactFiberRootSchedulerscheduleTaskForRootDuringMicrotask 里每次都会先调用:

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);
  }
}

调用链(同文件):

  • dispatchSetStateInternal
  • dispatchReducerAction

都会在 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 个结论

  1. Lane 是位图,不是优先级枚举:它天然支持"同批次 lanes"与"集合运算"。

  2. Root 上的 lanes 字段是调度事实表:pending/suspended/pinged/warm/expired 让 React 能在 suspend/恢复/speculative/兜底之间切换策略。

  3. getNextLanes 的核心不是"最高优先级",而是:

  • non-idle 优先
  • unblocked 优先
  • pinged 次之
  • 只有在没有 pending commit 压力时才做 prewarm
  • 避免无意义打断 wip
  1. 过期机制是对并发让步的安全阀markStarvedLanesAsExpired + includesExpiredLane 让 React 在极端情况下更强硬地完成更新。

  2. Entangle 是 Transition 语义的关键实现:并非为了"更快",而是为了"同源更新不展示中间态"。

  3. Async action scope 是工程折中:没有 async context 时,React 用"纠缠 scope + 共享 lane + render 时必要时 suspend"来维持一致的过渡体验。


下一篇预告

第 5 篇我会专门把"并发渲染下的 Suspense 与 retry/ping"抽出来:

  • attachPingListener / pingSuspendedRoot
  • RootSuspendedWithDelay 与 fallback throttle
  • retry lanes 如何与 commit/预热(prerender)机制协作
相关推荐
程序员清洒9 分钟前
Flutter for OpenHarmony:GridView — 网格布局实现
android·前端·学习·flutter·华为
VX:Fegn089512 分钟前
计算机毕业设计|基于ssm + vue超市管理系统(源码+数据库+文档)
前端·数据库·vue.js·spring boot·后端·课程设计
0思必得021 分钟前
[Web自动化] 反爬虫
前端·爬虫·python·selenium·自动化
LawrenceLan31 分钟前
Flutter 零基础入门(二十六):StatefulWidget 与状态更新 setState
开发语言·前端·flutter·dart
秋秋小事1 小时前
TypeScript 模版字面量与类型操作
前端·typescript
2401_892000521 小时前
Flutter for OpenHarmony 猫咪管家App实战 - 添加提醒实现
前端·javascript·flutter
Yolanda941 小时前
【项目经验】vue h5移动端禁止缩放
前端·javascript·vue.js
广州华水科技3 小时前
单北斗GNSS形变监测一体机在基础设施安全中的应用与技术优势
前端
EndingCoder3 小时前
案例研究:从 JavaScript 迁移到 TypeScript
开发语言·前端·javascript·性能优化·typescript
阿珊和她的猫4 小时前
React 路由:构建单页面应用的导航系统
前端·react.js·状态模式