Skip to content

第9章 并发模式深度解析

本章要点

  • 并发渲染的本质:不是多线程,而是可中断、可恢复的渲染模型
  • Lane 模型与优先级调度:从二进制位运算到任务插队的完整机制
  • Transition 的实现原理:entangled transitions 与状态一致性
  • Suspense 的挂起与恢复:Promise 协议、Offscreen Fiber 与回退策略
  • Selective Hydration:服务端渲染场景下的并发注水策略
  • Tearing 问题:并发模式下的状态撕裂与 useSyncExternalStore 的解决方案
  • 并发的本质不是"做得更快",而是"让用户感觉更快"

2022 年 3 月,React 18 正式发布。在所有的新特性中,有一个被反复提及却最常被误解的概念——并发模式(Concurrent Mode)。

许多开发者第一次听到"并发"这个词时,脑海中浮现的是操作系统课程里的多线程模型:多个线程同时执行,通过锁和信号量协调共享资源。这种直觉是危险的,因为 React 的并发与多线程完全无关。JavaScript 只有一个主线程,React 不能也不会创建新的线程。那么,React 的"并发"到底是什么?

答案是:可中断的渲染。在传统的同步渲染模型中,一旦 React 开始处理一次更新,它会一口气遍历整棵 Fiber 树,直到所有工作完成。在这个过程中,主线程被完全占用——用户的点击、输入、滚动都无法得到响应。并发模式改变了这个契约:React 可以开始渲染一棵树,在中途暂停,去处理更紧急的工作(比如用户输入),然后回来继续之前的渲染。更进一步地,React 甚至可以丢弃正在进行的渲染,转而开始一次全新的渲染。这种能力,是 startTransitionSuspenseuse 等所有现代 React API 的底层基石。

9.1 什么是并发渲染

9.1.1 同步渲染的天花板

在 React 18 之前,所有的渲染都是同步的。让我们用一个具体的场景来理解这意味着什么:

tsx
function SearchResults({ query }: { query: string }) {
  // 假设这个组件需要渲染 10000 个搜索结果
  const results = computeResults(query); // 耗时 200ms
  return (
    <ul>
      {results.map(item => (
        <li key={item.id}>{item.title}</li>
      ))}
    </ul>
  );
}

function SearchPage() {
  const [query, setQuery] = useState('');
  return (
    <div>
      <input
        value={query}
        onChange={e => setQuery(e.target.value)}
      />
      <SearchResults query={query} />
    </div>
  );
}

在同步渲染模式下,每次用户按下键盘,setQuery 触发的更新会立即开始一次完整的渲染。SearchResults 需要 200ms 来计算和渲染——在这 200ms 内,输入框无法响应用户的下一次击键。用户感受到的是明显的卡顿和输入延迟。

同步渲染的核心问题在于:它假设所有更新都具有相同的紧急程度。但在真实的用户交互中,"输入框立即显示用户输入的字符"和"搜索结果列表更新"显然不是同等紧急的事情。

下图对比了同步渲染与并发渲染的执行模型差异:

9.1.2 可中断渲染的工作模型

并发渲染的核心思想可以用一个类比来理解:想象你是一个厨师,正在准备一道需要 30 分钟的炖菜。在同步模式下,你必须站在锅前盯着 30 分钟,期间无法处理任何其他事情。在并发模式下,你可以先把锅放上火,然后去处理一个刚到的外卖订单(紧急任务),处理完后再回来继续看管炖菜。

在 React 的实现中,这种"中断"发生在 Fiber 节点的边界:

typescript
// packages/react-reconciler/src/ReactFiberWorkLoop.js
function workLoopConcurrent() {
  // 并发模式的工作循环:每处理一个 Fiber 都检查是否需要让出
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

function workLoopSync() {
  // 同步模式的工作循环:一口气做完所有工作
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}

区别只有一个条件——shouldYield()。这个来自 Scheduler 的函数检查当前时间切片(通常为 5ms)是否已经用完。如果用完了,React 会把控制权交还给浏览器,让浏览器有机会处理用户事件和渲染更新。

9.1.3 Lane 模型:并发的优先级引擎

下图展示了 Lane 模型的优先级层次结构,位越低(越靠右)优先级越高:

并发渲染的实现依赖于一个精密的优先级系统——Lane 模型。每次更新都被分配一个 Lane(车道),不同的 Lane 代表不同的优先级:

typescript
// packages/react-reconciler/src/ReactFiberLane.js
export const NoLanes: Lanes = /*                        */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /*                          */ 0b0000000000000000000000000000000;

export const SyncLane: Lane = /*                        */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lane = /*             */ 0b0000000000000000000000000001000;
export const DefaultLane: Lane = /*                     */ 0b0000000000000000000000000100000;

export const TransitionLane1: Lane = /*                 */ 0b0000000000000000000001000000000;
export const TransitionLane2: Lane = /*                 */ 0b0000000000000000000010000000000;
export const TransitionLane3: Lane = /*                 */ 0b0000000000000000000100000000000;
export const TransitionLane4: Lane = /*                 */ 0b0000000000000000001000000000000;
// ... 更多 Transition Lanes

export const RetryLane1: Lane = /*                      */ 0b0000001000000000000000000000000;
export const RetryLane2: Lane = /*                      */ 0b0000010000000000000000000000000;

export const OffscreenLane: Lane = /*                   */ 0b1000000000000000000000000000000;

这个设计极为巧妙。使用二进制位来表示优先级有几个关键优势:

  1. 集合操作极其高效:合并两个 Lane 只需位或 a | b,取交集用位与 a & b,检查是否包含用 a & b !== 0
  2. 同一优先级可以有多个车道:Transition 有 16 条车道,允许多个 Transition 同时存在而互不干扰
  3. 优先级比较通过位置判断:位越低(越靠右),优先级越高

基于 VitePress 构建