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)机制协作
相关推荐
码客前端1 分钟前
理解 Flex 布局中的 flex:1 与 min-width: 0 问题
前端·css·css3
Komorebi゛2 分钟前
【CSS】圆锥渐变流光效果边框样式实现
前端·css
工藤学编程14 分钟前
零基础学AI大模型之CoT思维链和ReAct推理行动
前端·人工智能·react.js
徐同保14 分钟前
上传文件,在前端用 pdf.js 提取 上传的pdf文件中的图片
前端·javascript·pdf
怕浪猫16 分钟前
React从入门到出门第四章 组件通讯与全局状态管理
前端·javascript·react.js
博主花神16 分钟前
【React】扩展知识点
javascript·react.js·ecmascript
欧阳天风23 分钟前
用setTimeout代替setInterval
开发语言·前端·javascript
EndingCoder27 分钟前
箭头函数和 this 绑定
linux·前端·javascript·typescript
郑州光合科技余经理27 分钟前
架构解析:同城本地生活服务o2o平台海外版
大数据·开发语言·前端·人工智能·架构·php·生活
沐墨染29 分钟前
大型数据分析组件前端实践:多维度检索与实时交互设计
前端·elementui·数据挖掘·数据分析·vue·交互