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. 优先级比较通过位置判断:位越低(越靠右),优先级越高
typescript
// 判断一组 Lanes 中最高优先级的 Lane
function getHighestPriorityLane(lanes: Lanes): Lane {
  // 位运算技巧:取最低位的 1
  return lanes & -lanes;
}

// 判断是否包含某个 Lane
function includesSomeLane(a: Lanes, b: Lanes): boolean {
  return (a & b) !== NoLanes;
}

// 合并两组 Lanes
function mergeLanes(a: Lanes, b: Lanes): Lanes {
  return a | b;
}

// 从集合中移除某些 Lanes
function removeLanes(set: Lanes, subset: Lanes): Lanes {
  return set & ~subset;
}

当 React 开始一次渲染时,它需要决定本次渲染要处理哪些 Lane。这个决策过程称为 getNextLanes,它的逻辑遵循严格的优先级层次:

typescript
function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
  const pendingLanes = root.pendingLanes;
  if (pendingLanes === NoLanes) return NoLanes;

  // 优先级决策的核心逻辑:
  // 1. 先看非空闲的 Lanes 中,有没有未被 Suspense 挂起的
  // 2. 如果都被挂起了,看有没有被 ping(Promise resolved)的
  // 3. 最后才考虑空闲优先级的 Lanes
  // 在每一层中,都取最高优先级的 Lane(最低位的 1)

  const nonIdlePendingLanes = pendingLanes & NonIdleLanes;
  if (nonIdlePendingLanes !== NoLanes) {
    const nonIdleUnblockedLanes = nonIdlePendingLanes & ~suspendedLanes;
    if (nonIdleUnblockedLanes !== NoLanes) {
      return getHighestPriorityLanes(nonIdleUnblockedLanes);
    }
    const nonIdlePingedLanes = nonIdlePendingLanes & pingedLanes;
    if (nonIdlePingedLanes !== NoLanes) {
      return getHighestPriorityLanes(nonIdlePingedLanes);
    }
  }

  return getHighestPriorityLanes(pendingLanes & ~suspendedLanes);
}

深度洞察:Lane 模型的设计灵感来自高速公路的车道系统。在高速公路上,不同车道有不同的速度限制——快车道(SyncLane)只允许高速通行,慢车道(TransitionLane)允许低速行驶。当快车道有车来时,慢车道上的车必须让路。React 的并发调度本质上就是这样一个车道管理系统:高优先级更新可以"超车"低优先级更新,而低优先级更新在等待太久后会被"提速"(过期机制),防止饥饿。

9.2 Transition 的实现机制

9.2.1 startTransition:标记低优先级更新

startTransition 是并发模式最核心的用户侧 API。它的作用看似简单——将一个状态更新标记为"非紧急":

tsx
function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);

  function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
    // 紧急更新:立即更新输入框
    setQuery(e.target.value);

    // 非紧急更新:搜索结果可以延迟
    startTransition(() => {
      setResults(computeResults(e.target.value));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      <ResultsList results={results} />
    </div>
  );
}

但在源码层面,startTransition 的实现远比你想象的复杂。它涉及 Lane 的分配、Transition 上下文的管理,以及跨更新的状态一致性保证:

typescript
// packages/react/src/ReactStartTransition.js
function startTransition(
  scope: () => void,
  options?: StartTransitionOptions
): void {
  const prevTransition = ReactSharedInternals.T;

  // 创建一个新的 Transition 对象
  const transition: BatchConfigTransition = {};
  ReactSharedInternals.T = transition;

  const currentTransition = ReactSharedInternals.T;

  if (__DEV__) {
    ReactSharedInternals.T._updatedFibers = new Set();
  }

  try {
    // 在 Transition 上下文中执行回调
    // 回调中的所有 setState 都会被分配 TransitionLane
    const returnValue = scope();

    // React 19: 支持异步 Transition
    if (
      typeof returnValue === 'object' &&
      returnValue !== null &&
      typeof returnValue.then === 'function'
    ) {
      // 异步 Transition:追踪 Promise 的完成
      entangleAsyncAction(transition, returnValue);
    }
  } finally {
    // 恢复之前的 Transition 上下文
    ReactSharedInternals.T = prevTransition;
  }
}

关键点在于 ReactSharedInternals.T。当这个值不为 null 时,所有通过 dispatchSetState 触发的更新都会被分配 TransitionLane,而不是默认的 SyncLane 或 DefaultLane:

typescript
// packages/react-reconciler/src/ReactFiberHooks.js
function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A
): void {
  const lane = requestUpdateLane(fiber);
  // ...
}

function requestUpdateLane(fiber: Fiber): Lane {
  // 检查是否在 Transition 上下文中
  const isTransition = ReactSharedInternals.T !== null;
  if (isTransition) {
    // 分配一个 Transition Lane
    const actionScopeLane = peekEntangledActionLane();
    return actionScopeLane !== NoLane
      ? actionScopeLane
      : requestTransitionLane();
  }

  // 非 Transition 的更新根据触发事件的类型确定优先级
  const updateLane: Lane = getCurrentUpdatePriority();
  if (updateLane !== NoLane) {
    return updateLane;
  }

  // 从事件系统获取优先级
  const eventLane: Lane = getCurrentEventPriority();
  return eventLane;
}

9.2.2 Entangled Transitions:交织的一致性

React 19 引入了一个关键概念——entangled transitions(交织的过渡)。当多个 Transition 更新共享相关状态时,React 必须确保它们作为一个整体被提交,而不是部分提交导致 UI 处于不一致的中间状态。

typescript
// packages/react-reconciler/src/ReactFiberLane.js
export function entangleTransitions(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane
): void {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    return;
  }

  const sharedQueue = updateQueue.shared;
  if (isTransitionLane(lane)) {
    let queueLanes = sharedQueue.lanes;

    // 将当前 Lane 与队列中已有的 Lanes 纠缠在一起
    queueLanes = intersectLanes(queueLanes, root.pendingLanes);

    const newQueueLanes = mergeLanes(queueLanes, lane);
    sharedQueue.lanes = newQueueLanes;

    // 在 root 上标记这些 Lanes 是纠缠的
    markRootEntangled(root, newQueueLanes);
  }
}

export function markRootEntangled(root: FiberRoot, entangledLanes: Lanes): void {
  // 在 root.entanglements 数组中,确保所有纠缠的 Lanes 互相引用
  // 遍历每个 Lane,如果它属于纠缠集合,就将整个纠缠集合加入它的关联列表
  root.entangledLanes |= entangledLanes;
  const entanglements = root.entanglements;
  let lanes = root.entangledLanes;
  while (lanes) {
    const index = pickArbitraryLaneIndex(lanes);
    const lane = 1 << index;
    if ((lane & entangledLanes) | (entanglements[index] & entangledLanes)) {
      entanglements[index] |= entangledLanes;
    }
    lanes &= ~lane;
  }
}

基于 VitePress 构建