# react18并发调度源码主线

# 这篇解决什么问题

很多人知道 React 18 有并发渲染,但一读源码就迷路:
lane、优先级、可中断渲染、commit 到底怎么串起来?

这篇给你一条“能落地的阅读主线”,重点回答三件事:

  • 更新是怎么被打上优先级的
  • 为什么渲染可以被中断与恢复
  • commit 为什么必须同步且不可中断

# 先建立总流程(从一次 setState 开始)

一次更新大致经过:

  1. 触发更新(setState / dispatch)
  2. 创建 update,并分配 lane(优先级位)
  3. 向 root 标记待处理 lane
  4. 调度器决定何时执行 render(可能并发)
  5. render 阶段构建 workInProgress 树(可中断)
  6. render 完成后进入 commit(同步、不可中断)
  7. 提交 DOM 变更 + 执行副作用(layout/passive)

你可以把它理解为:
lane 决定“先做谁”,scheduler 决定“何时做”,fiber 决定“如何做”。


# 一、Lane 模型:React 18 的优先级核心

# 1)为什么不用一个数字优先级,而用 lane 位掩码

lane 本质是 bitmask(位集合),不是单一值。
这样设计可以同时表达:

  • 多个不同优先级更新并存
  • 某些 lane 合并处理
  • 某些 lane 饥饿时被提升(过期)

这比“单任务队列 + 数字优先级”更适合 UI 更新场景。

# 2)常见 lane 语义(理解层)

不强求记常量名,先记语义层:

  • Sync 类:必须尽快完成(离散输入)
  • Default 类:常规更新
  • Transition 类:可延迟、可打断
  • Idle 类:空闲时再做

# 3)一次更新如何拿到 lane

requestUpdateLane(概念入口)会综合:

  • 当前事件优先级(点击、输入、滚动等)
  • 当前执行上下文(是否在 transition 内)
  • 现有 pending lane 状态

然后给 update 打上 lane,并向上冒泡到 root。

# 4)root 上的 lane 状态

root 会维护多组 lane 集合(可理解为状态桶):

  • pending:待处理
  • suspended:被挂起(如 Suspense)
  • pinged:挂起资源恢复后可重试
  • expired:超时需尽快处理

这套状态机是“可恢复并发”的关键基础。


# 二、优先级桥接:事件 -> lane -> Scheduler

React 内部有两套优先级语义:

  • React 语义优先级(lane)
  • Scheduler 任务优先级(Immediate/UserBlocking/Normal...)

调度时会把 lane 映射到 Scheduler 优先级,再把任务丢给调度器。

# 关键价值

  • React 保留了自身更新语义(lane)
  • 复用 Scheduler 做时间片与任务让渡

这就是“渲染语义”和“执行时机”解耦。


# 三、可中断渲染:为什么 render 可以停

# 1)render 阶段在做什么

render 主要是“算树”:

  • 从当前树(current)克隆/复用为 workInProgress
  • 执行 beginWork(算子节点)
  • 执行 completeWork(冒泡、收集 effect)

这阶段不直接改真实 DOM,所以可以中断。

# 2)work loop 与时间片

并发模式下,work loop 会周期性检查是否该让出主线程(shouldYield)。
如果时间片到、且有更高优任务进来,就先暂停当前 render。

# 3)恢复机制

因为 workInProgress 树保存了进度,下次可以从中断点继续。
如果有更高优 lane 更新,可能会重开一次 render(丢弃部分旧进度)。

# 4)为什么说“并发不是并行”

JS 仍是单线程。
并发在这里是“任务可切片、可抢占、可恢复”,不是多核并行执行。


# 四、Transition:可延迟更新的产品化接口

startTransition / useTransition 的本质:
把更新打到 transition lane,让它在高优交互后再推进。

典型场景:

  • 输入框内容立即更新(高优)
  • 大列表筛选结果延迟更新(transition)

这就是 React 18 “既快又稳”的核心手法:高优交互不被重渲染拖慢。


# 五、commit 阶段:为什么必须同步不可中断

render 可中断,但 commit 必须一次完成。原因很简单:
commit 会触达真实世界(DOM/Ref/生命周期副作用),中断会造成 UI 不一致。

# commit 三个子阶段(理解层)

  1. before mutation:提交前准备(如快照)
  2. mutation:真正改 DOM
  3. layout:执行 layout effect / ref 等同步副作用

之后异步调度 passive effects(useEffect)。

# 一句话记忆

  • render:可打断,算结果
  • commit:不可打断,落结果

# 六、Suspense 与并发调度如何配合

当 render 中遇到“数据未就绪”:

  • 对应 lane 可能进入 suspended
  • 展示 fallback
  • 数据 ready 后触发 ping,lane 进入可重试

这就是 Suspense 能和并发调度协同的底层机制:
不是“失败重来”,而是“挂起等待后定向恢复”。


# 七、源码阅读顺序建议(实操)

下面给一个“少走弯路”的阅读路径(按职责):

  1. 更新入口:setState/dispatch 到 update 创建
  2. lane 分配:requestUpdateLane 相关链路
  3. root 调度:ensureRootIsScheduled
  4. render 主循环:workLoopConcurrent / performUnitOfWork
  5. Fiber 两阶段:beginWork / completeWork
  6. commit:commitRoot 主流程
  7. effect:layout/passive 的执行时机
  8. Suspense:suspend -> ping -> retry

阅读方法建议:

  • 先画时序图,再看源码细节
  • 一次只追一个场景(如输入 + 列表 transition)
  • 不要第一天就纠结所有 lane 常量值

# 八、常见理解误区

# 误区 1:React 18 默认所有更新都并发

不是。并发是能力,不是所有更新的强制模式。

# 误区 2:有了并发就不会卡

并发只能改善调度与响应,不会消灭重计算本身。
昂贵计算仍需拆分、缓存、虚拟列表等手段。

# 误区 3:useTransition 能替代防抖节流

它解决的是渲染优先级,不是请求风暴治理,两者职责不同。


# 小结(主线回看)

React 18 并发调度可以压缩成一条主线:
update 打 lane -> scheduler 按优先级调度 -> render 可中断计算 -> commit 同步落地。

如果你把这条线吃透,再回头看 Fiber、Suspense、Transition,都会从“概念”变成“可推演的系统”。