Appearance
第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 甚至可以丢弃正在进行的渲染,转而开始一次全新的渲染。这种能力,是 startTransition、Suspense、use 等所有现代 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;这个设计极为巧妙。使用二进制位来表示优先级有几个关键优势:
- 集合操作极其高效:合并两个 Lane 只需位或
a | b,取交集用位与a & b,检查是否包含用a & b !== 0 - 同一优先级可以有多个车道:Transition 有 16 条车道,允许多个 Transition 同时存在而互不干扰
- 优先级比较通过位置判断:位越低(越靠右),优先级越高
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;
}
}