React 18 引入了并发功能,从根本上改变了渲染的工作方式。这些功能使 React 可以一次准备多个版本的 UI、中断正在进行的工作,并优先考虑紧急更新而不是非紧急更新。结果是应用程序响应速度更快,而无需放弃使 React 高效的声明式编程模型。
React 中的并发不是一种全有或全无的模式。与早期实验版本中废弃的“并发模式”概念不同,React 18+ 选择加入并发功能。您可以逐个采用它们,因为它们可以提供最大的价值。底层 Fiber 架构使这成为可能:React 的渲染阶段可以暂停和恢复,因此协调器可以随着优先级的变化在不同的工作单元之间切换。
使用转换钩子
useTransition 挂钩可让您将状态更新标记为非紧急。这可以防止更新在关键用户交互(例如打字)期间阻塞主线程。
import { useTransition } from "react";
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [query, setQuery] = useState("");
function handleChange(e) {
// Urgent: update the input value immediately
const value = e.target.value;
setQuery(value);
// Non-urgent: filter the large dataset
startTransition(() => {
setFilter(value);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<SearchResults filter={filter} />
</div>
);
}
isPending 标志提供了一种在不阻塞 UI 的情况下显示加载指示器的方法。当 React 正在进行转换时,isPending 是 true,用户可以继续键入,因为转换可能会被更紧急的状态更新中断。
转换与使用 setTimeout 进行去抖动有根本的不同。转换根本不会延迟执行。它降低了渲染工作的优先级,允许 React 暂停它,检查更高优先级的工作,并在线程空闲时恢复。去抖动总是会引入固定的延迟;当 CPU 空闲时,转换会引入零延迟。
useDeferredValue 钩子
其中 useTransition 包装状态设置器, useDeferredValue 包装值。当您无法轻松包装状态更新本身但仍希望保持 UI 响应时,这非常有用。
import { useDeferredValue, useMemo } from "react";
function SearchResults({ query, data }) {
const deferredQuery = useDeferredValue(query);
const isStale = query !== deferredQuery;
const filtered = useMemo(
() => expensiveFilter(data, deferredQuery),
[deferredQuery, data]
);
return (
<div style={{ opacity: isStale ? 0.5 : 1 }}>
{filtered.map((item) => (
<ListItem key={item.id} item={item} />
))}
</div>
);
}
当 CPU 空闲时,延迟值会立即更新,而不会在固定延迟之后更新。这会创建一个“陈旧状态”模式:用户在计算新结果时看到以前的结果(稍微变暗),而不是空白的加载旋转器。这种模式提供了一种感觉上更流畅的体验,因为 UI 永远不会变空。
| 方法 | 延迟 | 用户体验 |
|---|---|---|
| 同步渲染 | 每次击键都充满 | UI 在过滤期间冻结 |
| 去抖(300 毫秒) | 固定 300 毫秒延迟 | 输入感觉滞后 |
| 占位符_0 | 空闲时零延迟 | 反应灵敏输入,平滑过渡 |
并发渲染的悬念
在并发渲染下,悬念变得更加强大。在同步模型中,当组件挂起时,Suspense 边界上方的整个树都会回落到加载状态。通过并发功能,同级组件在解析挂起的组件时保持交互。
function Page() {
return (
<Suspense fallback={<PageSkeleton />}>
<Sidebar />
<Suspense fallback={<DetailSkeleton />}>
<MainContent />
</Suspense>
</Suspense>
);
}
嵌套 Suspense 边界可让您控制加载状态的粒度。 Sidebar 可以在 MainContent 流入时立即渲染。与 startTransition 结合使用,您可以在加载下一个屏幕时显示上一个 UI,从而消除困扰传统导航的“加载闪现”。
startTransition 外部钩子
startTransition 函数也可以作为独立导入在 React 组件之外使用。这对于状态管理库、路由器事件处理程序以及任何钩子不可用的代码路径都很有价值。
import { startTransition } from "react";
import { useNavigate } from "react-router-dom";
function navigateWithTransition(path) {
startTransition(() => {
// Wrap the navigation in a transition
// so the previous view stays visible
navigate(path);
});
}
您可以将 Zustand 或 Redux 存储更新包装在 startTransition 中,以降低非紧急状态更改的优先级。当存储更新触发许多订阅者之间昂贵的派生计算时,这尤其有用。
构建响应式搜索组件
实用的搜索组件将所有并发功能组合在一起。输入状态紧急。过滤器计算使用 useDeferredValue。导航到详细信息页面使用路由器上的 startTransition 。搜索结果本身被包裹在悬念边界中。
这是一个完整的模式:
function SearchContainer() {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input);
function handleClear() {
setInput("");
// The deferred value will catch up naturally
}
return (
<Suspense fallback={<ResultsSkeleton />}>
<SearchInput
value={input}
onChange={setInput}
onClear={handleClear}
/>
<SearchResults query={deferredInput} />
</Suspense>
);
}
当用户快速键入时,只有最后一次击键才会触发完整渲染。所有中间击键都会中断之前的转换。这避免了“卡顿搜索”问题,即打字感觉缓慢,因为每次击键都会触发昂贵的过滤器通过。
高级模式和最佳实践
对于分层优先级控制,请将 useTransition 与 useDeferredValue 组合起来。使用 useTransition 进行导航级状态更改(选项卡切换、页面转换),使用 useDeferredValue 进行视图内的派生计算。
要测试并发功能,请使用带有 act 包装器的 React 测试库:
import { act, render, screen } from "@testing-library/react";
await act(async () => {
render(<SearchPage />);
});
// Assert on the pending state
expect(screen.getByText("Loading...")).toBeInTheDocument();
优化前的配置文件。 React DevTools Profiler 显示渲染是否被中断以及每个渲染阶段消耗了多少时间。过早使用并发功能会增加复杂性,而不会带来可衡量的好处 - 始终首先进行测量,然后在解决真正瓶颈的地方应用转换和延迟值。
兼容性和迁移
并发功能需要 React 18+ 并且完全可以选择加入。升级时不会破坏现有组件。建议的迁移路径从高延迟用户交互开始:搜索输入、过滤大型列表和选项卡切换。一旦这些顺利进行,就扩展到导航转换和数据加载模式。
一些同步改变状态的第三方库可能不尊重转换边界。在这些上下文中采用转换之前测试库兼容性。
概括
并发功能为您提供了三个强大的工具:用于取消状态更新优先级的 useTransition、用于推迟派生计算的 useDeferredValue 以及用于优雅加载状态的并发 Suspense。每个解决特定类别的响应问题。首先进行分析,选择正确的工具,并提供即使在繁重的计算下也感觉即时的 UI。

