11.7 React 并发特性:Concurrent Mode、Lane 模型、Time Slicing、Suspense
Concurrent Mode、Lane 模型、Time Slicing、Suspense
原理
React 18 正式引入了并发特性(Concurrent Features),这是一套底层架构能力,允许 React 同时准备多个版本的 UI,并根据优先级决定何时展示哪个版本。它不是可选模式,而是 React 18 的默认行为。
Time Slicing(时间切片)
Time Slicing 是并发特性的基础能力。它将大型渲染任务拆分为多个小片(chunk),在每个时间片(通常 5ms 左右)后让出主线程,检查是否有更高优先级的任务(如用户输入)需要处理。
实现机制:
- React 使用
MessageChannel(或setTimeout降级)实现宏任务调度。 - 每个 Fiber 工作单元执行后检查是否超时(
shouldYield())。 - 若超时或存在更高优先级更新,保存当前进度,让出主线程。
这使得 React 应用即使在处理大量数据时,也能保持对用户输入的响应。
Lane 模型
React 18 使用 Lane 模型替代了之前的 Expiration Time 模型来管理更新优先级。Lane 使用位掩码(bitmask)表示,每个位代表一种更新类型:
- SyncLane:同步更新,最高优先级(如 React DOM 事件处理)。
- InputContinuousLane:连续输入(如
onMouseMove、onChange)。 - DefaultLane:默认更新(如
setState)。 - TransitionLane:过渡更新(如
startTransition包裹的更新)。 - IdleLane:空闲时执行的更新。
Lane 模型的优势:
- 优先级比较通过位运算实现,效率极高。
- 多个 Lane 可以合并(按位或),也可以拆分(按位与)。
- 支持批量处理同一优先级的多个更新。
startTransition
startTransition 是 React 18 提供的显式 API,用于标记非紧急更新:
import { startTransition } from 'react';
function handleChange(e) {
const value = e.target.value;
// 紧急更新:输入框必须立即响应
setInputValue(value);
// 非紧急更新:搜索结果可以延迟
startTransition(() => {
setSearchResults(filterLargeList(value));
});
}
被 startTransition 包裹的更新会被分配较低的 TransitionLane 优先级。如果期间发生更高优先级的更新(如继续输入),React 会中断过渡更新,优先处理紧急更新。
useTransition Hook
useTransition 提供了 startTransition 函数和 isPending 状态:
const [isPending, startTransition] = useTransition();
return (
<>
{isPending && <Spinner />}
<Results data={results} />
</>
);
Suspense 与数据获取
Suspense 允许组件在"等待"某些内容(如异步数据、懒加载组件、图片)时显示 fallback UI:
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<Loading />}>
<ProfileData />
</Suspense>
);
}
Suspense 的工作机制:
- 组件在渲染时抛出 Promise(或从缓存中读取未就绪的数据)。
- React 捕获该 Promise,暂停该子树的渲染。
- 显示最近的 Suspense fallback。
- Promise 解决后,React 恢复渲染子树。
React 18 扩展了 Suspense 的能力:
- Suspense 与 Transition 结合:
startTransition中的 Suspense 不会立即显示 fallback,而是保留旧 UI 直到新数据就绪(避免加载状态闪烁)。 - SuspenseList(实验性):控制多个 Suspense 边界的显示顺序。
useDeferredValue
useDeferredValue 用于延迟更新某个值,类似于防抖但由 React 调度:
const deferredQuery = useDeferredValue(query);
// query 变化时,先使用旧 deferredQuery 渲染(保持响应)
// 然后在空闲时更新为新的 deferredQuery
const results = useMemo(() => search(deferredQuery), [deferredQuery]);
与 startTransition 的区别:useDeferredValue 延迟的是值,而 startTransition 延迟的是状态更新。
并发特性不是并发渲染
React 的"并发"并非指多线程并行渲染(JavaScript 是单线程的),而是指 React 可以在内存中同时准备多个版本的 UI(多棵树),并根据优先级决定提交哪一个。这是一种"可中断的渲染",而非真正的并行计算。
用法
// 结合 Suspense 和 startTransition 实现流畅的 Tab 切换
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
const selectTab = (nextTab) => {
startTransition(() => {
setTab(nextTab);
});
};
return (
<>
<TabList activeTab={tab} onSelect={selectTab} />
{isPending && <TabSkeleton />}
<Suspense fallback={<TabSkeleton />}>
<TabContent tab={tab} />
</Suspense>
</>
);
}
实践
数据获取模式演进
React 18 推荐的 Suspense 数据获取模式:
- Fetch-on-Render:组件挂载后发起请求(传统 useEffect 模式)。
- Fetch-Then-Render:在路由或父组件中预取数据,再渲染。
- Render-as-You-Fetch:开始渲染后立即发起请求,Suspense 处理等待状态(推荐)。
框架如 Next.js App Router、Remix 已内置支持 Render-as-You-Fetch 模式。
并发模式下的状态一致性
在并发模式下,一个组件可能在渲染过程中被多次调用(因为高优先级更新打断了低优先级更新)。因此,Render Phase 必须是纯函数:
- 不要在 render 中修改外部变量。
- 不要在 render 中发起网络请求。
- 使用
useMemo缓存计算,但确保计算是纯的。
严格模式的双重调用
React 18 的严格模式在开发环境下会故意双重调用某些函数(如渲染函数、某些 effect 的清理和重新执行),以帮助检测副作用。这是为了模拟并发模式下组件可能被多次调用的情况。
陷阱
| 陷阱 | 现象 | 解决方案 | |------|------|---------| | startTransition 中执行同步 setState | 过渡效果失效 | 将紧急更新放在 startTransition 外 | | Suspense 边界放置不当 | 整个页面闪烁 fallback | 将 Suspense 放在更细粒度的组件边界 | | 在 render 中创建 Promise | 无限循环渲染 | 使用数据缓存层(如 React Query、SWR) | | 并发模式下的全局变量修改 | 状态不一致、难以复现的 Bug | 将状态放入 React state 或 ref | | 忽略 useDeferredValue 的依赖 | 延迟值未更新 | 确保传入的值变化会触发延迟更新 |