引言
React 18 的并发模式(Concurrent Mode)不是一项单一功能,而是一套底层重构后的调度体系。它的核心目标只有一个:在保证 UI 正确性的前提下,让高优先级更新能够中断低优先级渲染,从而避免组件树过于庞大时出现的卡顿。本文从 Fiber 架构出发,逐步深入 Work Loop、Lane 优先级模型、Suspense 边界以及双缓冲机制,帮助读者理解 React 并发渲染的完整链路。
一、Fiber 架构:链表树结构
在 React 16 之前,调和过程(Reconciliation)采用递归遍历组件树的方式,一旦开始便无法中断。当组件层级深、节点数量大时,主线程被长时间占用,导致动画掉帧、输入响应延迟。Fiber 架构彻底改变了这一局面。
1.1 Fiber 节点结构
每个 React 元素对应一个 Fiber 节点,节点之间通过链表连接:
// 简化版 Fiber 节点定义
function FiberNode(tag, pendingProps, key, mode) {
this.tag = tag; // 组件类型标识(FunctionComponent/ClassComponent等)
this.key = key;
this.elementType = null; // 创建该 Fiber 的 ReactElement.type
this.type = null; // 对应组件的函数或类
this.stateNode = null; // 对于 HostComponent,指向真实 DOM 节点
// 链表指针 —— 构成树结构的核心
this.return = null; // 父节点
this.child = null; // 第一个子节点
this.sibling = null; // 下一个兄弟节点
this.index = 0; // 在兄弟节点中的位置
this.pendingProps = pendingProps; // 待渲染的 props
this.memoizedProps = null; // 上一次渲染的 props
this.memoizedState = null; // 上一次渲染的 state
this.updateQueue = null; // 更新队列
this.flags = NoFlags; // 副作用标记(Placement/Update/Deletion 等)
this.lanes = NoLanes; // 该 Fiber 的更新优先级
this.alternate = null; // 指向另一棵树的对应节点(双缓冲核心)
}
与传统的树节点(同时拥有 left、right 或 children 数组)不同,Fiber 采用单链表 + 父指针的设计。组件树的遍历不再是递归调用栈,而是通过指针遍历完成的循环。这使得渲染过程随时可以从当前 Fiber 节点恢复,天然支持中断和恢复。
1.2 链表遍历策略
Fiber 树的遍历遵循深度优先规则,但使用显式的指针而非函数栈:
// 深度优先遍历的核心逻辑(简化版)
function performWorkOnFiber(fiber) {
// 1. 处理当前节点
const next = beginWork(fiber);
if (next !== null) {
// 有子节点,继续深入
return next;
}
// 2. 叶子节点,开始回溯
let node = fiber;
while (node !== null) {
completeWork(node);
// 有兄弟节点则转向兄弟
if (node.sibling !== null) {
return node.sibling;
}
// 否则回到父节点继续回溯
node = node.return;
}
return null; // 整棵树处理完毕
}
这种模式被称为 Unit of Work(工作单元) —— 每个 Fiber 节点就是一个可独立执行的工作单元。React 可以在这个链表的任意位置暂停,将控制权交还给浏览器。
二、时间切片:为什么 Fiber 能做到不阻塞
Fiber 的本质是将不可中断的大任务拆分为可中断的小任务。浏览器每帧约有 16.6ms(60fps),React 不再一次性构建整棵虚拟 DOM 树,而是在每个时间片内只执行少量工作单元,随后检查是否有更高优先级的任务需要处理。
// React Scheduler 的核心时间片逻辑(概念版)
const FRAME_YIELD_MS = 5; // 每帧留给 React 的工作时长
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function shouldYield() {
// 检查是否已超出当前时间片预算
return getCurrentTime() >= deadline;
}
当 shouldYield() 返回 true 时,React 立即保存当前执行位置(当前正在处理的 Fiber 节点),将主线程交还给浏览器执行布局、绘制和用户事件响应。下一次调度时,直接从保存的位置继续。这种机制正是并发渲染的基石。
三、Work Loop:beginWork 与 completeWork
React 的渲染分为两个阶段(Phase):
- Render Phase(可中断):构建 Fiber 树,计算新的 state 和 props,标记副作用。此阶段可以被打断和重新执行。
- Commit Phase(不可中断):将 Render Phase 计算的副作用应用到真实 DOM,执行生命周期和 effect。此阶段必须同步一次性完成。
3.1 beginWork —— 向下遍历
beginWork 接收当前 Fiber 节点,根据组件类型执行不同的处理逻辑,并返回下一个需要处理的子 Fiber 节点:
function beginWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case FunctionComponent: {
const Component = workInProgress.type;
const resolvedProps = workInProgress.pendingProps;
// 执行函数组件,获取子树
const children = Component(resolvedProps);
// 协调(reconcile)子节点
reconcileChildren(current, workInProgress, children, renderLanes);
return workInProgress.child;
}
case ClassComponent: {
const instance = workInProgress.stateNode;
const children = instance.render();
reconcileChildren(current, workInProgress, children, renderLanes);
return workInProgress.child;
}
case HostComponent: {
// div、span 等原生 DOM 节点
reconcileChildren(current, workInProgress, workInProgress.pendingProps.children, renderLanes);
return workInProgress.child;
}
case HostText: {
// 纯文本节点,无子节点,返回 null 触发回溯
return null;
}
// ... 其他组件类型
}
}
3.2 completeWork —— 向上回溯
当 beginWork 返回 null(到达叶子节点)时,触发 completeWork。在这个阶段,React 完成节点属性的计算,并为 HostComponent 创建或更新真实的 DOM 节点:
function completeWork(current, workInProgress, renderLanes) {
switch (workInProgress.tag) {
case HostComponent: {
const type = workInProgress.type;
const newProps = workInProgress.pendingProps;
if (current !== null && workInProgress.stateNode !== null) {
// 更新现有 DOM 节点
updateHostComponent(current, workInProgress, type, newProps);
} else {
// 创建新 DOM 节点
const instance = document.createElement(type);
// 将子树挂接到新节点上
appendAllChildren(instance, workInProgress, false);
workInProgress.stateNode = instance;
}
return null;
}
case FunctionComponent: {
// 函数组件无 DOM 节点,将副作用标记冒泡给父节点
bubbleProperties(workInProgress);
return null;
}
}
}
Render Phase 完成后,React 进入 Commit Phase,一次性将所有 DOM 变更写入页面,确保用户不会看到中间态。
四、Lane 优先级模型:31 条优先级车道
在 React 18 之前,更新优先级使用 expirationTime 模型,其本质是为每个更新分配一个到期时间戳,用时间数值的大小表示优先级高低。这个模型存在语义不够精确、无法批量处理同优先级更新的问题。React 18 引入了 Lane 模型,用 31 位二进制数的位运算来管理优先级,更加高效且语义清晰。
4.1 Lanes 的定义
// 31 个优先级车道(部分核心定义)
const TotalLanes = 31;
export const NoLanes = 0b0000000000000000000000000000000;
export const NoLane = 0b0000000000000000000000000000000;
export const SyncLane = 0b0000000000000000000000000000001; // 同步更新(useLayoutEffect 触发)
export const InputContinuousHydrationLane = 0b0000000000000000000000000000010;
export const InputContinuousLane = 0b0000000000000000000000000000100; // 连续输入事件
export const DefaultHydrationLane = 0b0000000000000000000000000001000;
export const DefaultLane = 0b0000000000000000000000000010000; // 默认更新
export const TransitionHydrationLane = 0b0000000000000000000000000100000;
export const TransitionLane1 = 0b0000000000000000000000001000000;
export const TransitionLane2 = 0b0000000000000000000000010000000;
// ... TransitionLane3 ~ TransitionLane15
export const RetryLane = 0b0001000000000000000000000000000; // Suspense 重试
export const SelectiveHydrationLane = 0b0010000000000000000000000000000;
export const IdleHydrationLane = 0b0100000000000000000000000000000;
export const IdleLane = 0b1000000000000000000000000000000; // 空闲时执行
每个更新被分配到一个或多个 Lane 上。由于使用位掩码,合并多个 Lane 只需按位或运算,检查交集只需按位与运算,性能极高。
4.2 批量更新与过渡通道
import { useState, startTransition } from 'react';
function SearchResults() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const handleInput = (e) => {
const value = e.target.value;
setQuery(value); // 分配到 SyncLane,立即更新输入框
// startTransition 将此更新分配到 TransitionLane
startTransition(() => {
setResults(search(value)); // 可能被中断,优先保证输入框流畅
});
};
return (
<div>
<input value={query} onChange={handleInput} />
<ResultsList data={results} />
</div>
);
}
在 Lane 模型中,setQuery 进入高优先级的 SyncLane,setResults 进入低优先级的 TransitionLane。当 React 调度更新时,如果用户继续输入,TransitionLane 上的更新会被 SyncLane 的更新打断,重新从更高优先级开始渲染。这确保了用户交互永远不卡顿。
五、Suspense 与边界:Promise 如何中断渲染
Suspense 不是单纯的加载占位符,它依赖于 React 内部对异常(其实是 Promise)的捕获机制。当一个组件在 Render Phase 中需要异步数据但数据尚未就绪时,它可以直接抛出一个 Promise。React 在渲染过程中捕获到这个 Promise 后,会暂停当前子树的渲染,并在该 Promise resolve 后恢复。
5.1 Suspense 的基本使用
import { Suspense } from 'react';
function UserProfile({ userId }) {
// 读取缓存中的用户数据,若未就绪则直接 throw promise
const user = readUser(userId);
return <div>{user.name}</div>;
}
function App() {
return (
<Suspense fallback={<Loading />}>
<UserProfile userId={123} />
</Suspense>
);
}
5.2 内部实现原理
React 为每个 Fiber 节点维护一个 suspenseState。当子树中抛出的 Promise 被 Suspense 边界捕获时,React 会将该边界以下的渲染标记为暂停状态:
// 捕获和处理 Promise 的核心逻辑(概念版)
function tryRender(root) {
try {
workLoop();
} catch (thrownValue) {
if (thrownValue !== null && typeof thrownValue.then === 'function') {
// 捕获到 Promise,这是一个 Suspend
const wakeable = thrownValue;
const suspenseBoundary = findNearestSuspenseBoundary(workInProgress);
// 将边界标记为需要显示 fallback
suspenseBoundary.flags |= DidCapture;
suspenseBoundary.memoizedState = {
dehydrated: null,
retryLane: DefaultLane,
};
// 注册 Promise 完成后的回调
attachRetryListener(suspenseBoundary, wakeable);
throwException(root, returnFiber, workInProgress, thrownValue, renderLanes);
return;
}
throw thrownValue; // 真正的异常,继续向上抛出
}
}
Promise 的 resolve 会触发一个高优先级的重试更新,React 重新进入渲染流程,此时缓存命中,正常完成渲染。整个过程开发者无感知,但底层经历了完整的"渲染—暂停—恢复"生命周期。
六、useTransition 与 useDeferredValue:保持响应性
useTransition 和 useDeferredValue 是 React 18 提供的两个并发钩子,它们都依赖 Transition Lane 的低优先级特性。
6.1 useTransition
useTransition 返回一个 isPending 标志和一个 startTransition 函数,用于标记那些可以被延迟的 UI 更新:
import { useTransition, useState } from 'react';
function TabContainer() {
const [isPending, startTransition] = useTransition();
const [tab, setTab] = useState('home');
const switchTab = (newTab) => {
startTransition(() => {
setTab(newTab); // 低优先级,允许被交互打断
});
};
return (
<div>
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<TabContent tab={tab} />
</div>
<button onClick={() => switchTab('home')}>Home</button>
<button onClick={() => switchTab('settings')}>Settings</button>
</div>
);
}
当用户点击按钮切换标签时,旧标签的卸载和新标签的渲染被分配到低优先级的 TransitionLane。如果用户在这期间点击了其他交互元素,这些交互会立即获得高优先级,中断标签的切换渲染。
6.2 useDeferredValue
useDeferredValue 适用于那些由 props 驱动的重渲染场景。它创建一个延迟版本的值,在并发渲染中,原始值会先保证 UI 响应,延迟值在后台慢慢更新:
import { useDeferredValue, memo } from 'react';
const SlowList = memo(function SlowList({ text }) {
const items = []
for (let i = 0; i < 250; i++) {
items.push(<SlowItem key={i} text={text} />);
}
return <ul>{items}</ul>;
});
function App() {
const [text, setText] = useState('');
const deferredText = useDeferredValue(text); // 延迟值
const isStale = text !== deferredText; // 是否仍在处理旧值
return (
<>
<input value={text} onChange={e => setText(e.target.value)} />
<SlowList text={deferredText} />
{isStale && <div style={{ opacity: 0.5 }}>Loading...</div>}
</>
);
}
在这个例子中,用户输入时 text 立即更新,但 deferredText 的更新会被延后。如果 SlowList 渲染耗时较长,React 可以中断它并优先处理新的输入更新,避免输入框卡顿。
七、双缓冲:current 树与 workInProgress 树
React 采用**双缓冲(Double Buffering)**策略,始终维护两棵 Fiber 树:
- current 树:当前屏幕上正在展示的 UI 对应的 Fiber 树,每个 Fiber 节点的
alternate指向 workInProgress 树的对应节点。 - workInProgress 树:正在构建中的新 Fiber 树, Render Phase 中的所有修改都发生在这棵树上。
// current 与 workInProgress 的交替切换
function createWorkInProgress(current, pendingProps) {
let workInProgress = current.alternate;
if (workInProgress === null) {
// 首次渲染,创建新的 workInProgress 节点
workInProgress = createFiber(current.tag, pendingProps, current.key, current.mode);
workInProgress.elementType = current.elementType;
workInProgress.type = current.type;
workInProgress.stateNode = current.stateNode;
workInProgress.alternate = current;
current.alternate = workInProgress;
} else {
// 复用已有的 alternate 节点
workInProgress.pendingProps = pendingProps;
workInProgress.flags = NoFlags;
workInProgress.lanes = NoLanes;
}
// 复制必要属性
workInProgress.child = current.child;
workInProgress.memoizedState = current.memoizedState;
workInProgress.memoizedProps = current.memoizedProps;
return workInProgress;
}
Render Phase 完成后,workInProgress 树已经完整构建。Commit Phase 中,React 只需将 root.current 指针从 current 树切换到 workInProgress 树,即可完成 UI 更新的原子切换:
function commitRoot(root) {
const finishedWork = root.finishedWork;
// 切换根节点的 current 指针
root.current = finishedWork;
// 执行 DOM 操作和 effect
commitMutationEffects(root, finishedWork);
commitLayoutEffects(finishedWork, root);
}
双缓冲的设计保证了用户在 Render Phase 中永远不会看到中间状态。即使渲染被多次中断和重启,屏幕上始终呈现完整且一致的 current 树。
八、React Scheduler:统一调度层
React 自己实现了一个独立于框架的 Scheduler 包,用于管理所有异步任务的优先级和执行时机。它既可以作为 requestIdleCallback 的 polyfill,也提供了 MessageChannel 为基础的高精度调度能力。
8.1 调度队列
Scheduler 维护一个基于最小堆实现的优先级队列,任务按过期时间排序,最早过期的任务最先执行:
// Scheduler 任务优先级定义
export const NoPriority = 0;
export const ImmediatePriority = 1; // 同步,立即执行
export const UserBlockingPriority = 2; // 用户交互(点击、输入)
export const NormalPriority = 3; // 普通异步更新
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲时才执行
function unstable_scheduleCallback(priorityLevel, callback, options) {
const currentTime = getCurrentTime();
const startTime = options?.delay ? currentTime + options.delay : currentTime;
// 根据优先级计算过期时间
let timeout;
switch (priorityLevel) {
case ImmediatePriority: timeout = -1; break;
case UserBlockingPriority: timeout = 250; break;
case NormalPriority: timeout = 5000; break;
case LowPriority: timeout = 10000; break;
case IdlePriority: timeout = -1; break;
}
const expirationTime = startTime + timeout;
const newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
push(timerQueue, newTask); // 先进入定时队列
requestHostCallback(flushWork); // 触发调度
return newTask;
}
8.2 时间切片实现
Scheduler 不直接依赖 requestIdleCallback,因为该 API 存在调用频率低和浏览器兼容性不足的问题。React 使用 MessageChannel 实现了一个微调的循环:
const channel = new MessageChannel();
const port = channel.port2;
// 注册回调
channel.port1.onmessage = performWorkUntilDeadline;
function requestHostCallback(callback) {
scheduledHostCallback = callback;
port.postMessage(null); // 触发宏任务
}
function performWorkUntilDeadline() {
const currentTime = getCurrentTime();
startTime = currentTime;
// 持续执行任务直到时间片用完
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (hasMoreWork) {
// 还有任务,立即安排下一次回调
port.postMessage(null);
}
}
MessageChannel 的回调在宏任务队列中执行,不会阻塞微任务和用户事件,同时相比 setTimeout 有更高的精度和更低的延迟。
结语
React 并发模式的每一项功能——Suspense、useTransition、DeferredValue——都不是孤立的 API,而是建立在 Fiber 架构、Lane 优先级模型和 Scheduler 调度层之上的统一体系。Fiber 的可中断链表遍历让时间切片成为可能,Lane 的位运算让优先级比较和合并变得高效,双缓冲保证了原子化的 UI 切换,而 Suspense 的 Promise 抛出机制则将异步数据加载无缝集成到渲染循环中。
理解这些底层原理,不仅有助于写出更高效的 React 应用,也能在遇到性能瓶颈时,准确判断问题究竟是出在渲染层、调度层还是数据层,从而做出有针对性的优化。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。