说说对 React 并发模式的理解?
一、是什么
并发渲染(Concurrent Rendering)是 React 18 引入的一种新的渲染机制。需要特别澄清的是,React 团队已不再使用"并发模式(Concurrent Mode)"这一术语——并发渲染不是一个开关式的模式,而是一种底层能力,只有在使用了并发特性(如 useTransition、useDeferredValue)时才会激活。
在传统的同步渲染中,一旦 React 开始渲染一棵组件树,它必须完成所有工作后才能响应用户交互。并发渲染打破了这个限制——React 可以暂停正在进行的渲染工作,去处理更紧急的更新,然后再恢复之前的渲染。
二、同步渲染 vs 并发渲染
2.1 同步渲染的问题
function App() {
const [query, setQuery] = useState('');
const [list, setList] = useState([]);
function handleChange(e) {
const value = e.target.value;
setQuery(value);
// 假设 filterItems 需要遍历 10000 个项目
setList(filterItems(value));
}
return (
<div>
<input value={query} onChange={handleChange} />
<ItemList items={list} />
</div>
);
}
在同步渲染下,setQuery 和 setList 的更新在同一次渲染中处理。如果 filterItems 和 ItemList 渲染耗时较长,用户的输入会出现明显卡顿——因为浏览器在 React 完成渲染前无法响应后续输入。
2.2 并发渲染如何解决
并发渲染下,React 可以:
- 中断:低优先级的渲染(如列表更新)可以被高优先级的更新(如输入框响应)中断
- 丢弃:如果新的更新到来,React 可以丢弃正在进行但已过时的渲染工作
- 恢复:在高优先级任务完成后,恢复之前被中断的渲染
三、时间切片(Time Slicing)
时间切片是并发渲染的实现手段。React 将渲染工作分割成小的工作单元,每完成一个单元就检查是否有更高优先级的任务需要处理:
同步渲染(阻塞):
[========= 渲染整棵树 =========][浏览器绘制]
↑ 用户无法交互
并发渲染(可中断):
[渲染A][让出][渲染B][让出][渲染C][浏览器绘制][渲染D]...
↑ ↑ ↑
检查是否有更紧急任务
React 使用 MessageChannel(而非 requestIdleCallback)实现任务调度,默认每帧留给 React 约 5ms 的时间片。
四、优先级与 Lanes 模型
React 18 使用 Lanes(车道)模型来管理更新优先级。不同的更新被分配到不同的 Lane,优先级从高到低:
function SearchPage() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isPending, startTransition] = useTransition();
function handleChange(e) {
// 高优先级:立即更新输入框
setQuery(e.target.value);
// 低优先级(TransitionLane):可以被中断
startTransition(() => {
setResults(searchItems(e.target.value));
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <Spinner />}
<ResultList results={results} />
</div>
);
}
在这个例子中,输入框的更新(setQuery)获得高优先级,搜索结果的更新(setResults)获得低优先级。即使搜索结果渲染很慢,输入框依然保持流畅响应。
五、如何启用并发渲染
并发渲染并不需要"开启"——只需使用 createRoot 作为渲染入口,React 就具备了并发渲染的能力:
import { createRoot } from 'react-dom/client';
// createRoot 使应用具备并发渲染能力
const root = createRoot(document.getElementById('root'));
root.render(<App />);
但只有在代码中使用了并发特性时,React 才会实际进行并发渲染:
import { useTransition, useDeferredValue, startTransition } from 'react';
// 方式一:useTransition Hook
function TabContainer() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function switchTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<TabBar onSwitch={switchTab} isPending={isPending} />
<TabContent tab={tab} />
</div>
);
}
// 方式二:useDeferredValue
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
return <HeavyList filter={deferredQuery} />;
}
// 方式三:模块级 startTransition(不需要 isPending)
import { startTransition } from 'react';
startTransition(() => {
setState(newValue);
});
六、与 Suspense 的集成
并发渲染与 Suspense 深度集成。在同步渲染中,Suspense 挂起时会立即显示 fallback;在并发渲染中,React 可以选择继续显示旧内容,而在后台准备新内容:
function App() {
const [tab, setTab] = useState('home');
const [isPending, startTransition] = useTransition();
function switchTab(nextTab) {
startTransition(() => {
setTab(nextTab);
});
}
return (
<div>
<TabBar active={tab} onSwitch={switchTab} />
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<Suspense fallback={<Loading />}>
{tab === 'home' && <HomePage />}
{tab === 'posts' && <PostsPage />}
{tab === 'settings' && <SettingsPage />}
</Suspense>
</div>
</div>
);
}
当用户切换 Tab 时:
- 不使用 Transition:立即切到 fallback(Loading),数据就绪后显示内容
- 使用 Transition:继续显示当前 Tab(略微变暗表示 pending),新 Tab 在后台渲染就绪后再切换
七、并发渲染的心智模型
可以将并发渲染想象为 Git 分支:
主分支(屏幕显示): A ────────────────── B(紧急更新结果)
\ ↗
特性分支(后台渲染): └── C ── D ──
(transition 渲染,可被丢弃或合并)
- React 在"后台分支"上进行 transition 渲染
- 渲染过程中不会对屏幕产生任何影响
- 如果有新的紧急更新,可以中断后台分支,先处理主分支
- 后台分支完成后,将结果一次性"合并"到屏幕
八、实际影响与注意事项
8.1 渲染可能执行多次
在并发模式下,render 阶段(函数组件的函数体)可能被执行多次然后丢弃。因此必须确保渲染逻辑是纯函数:
// 正确:纯渲染逻辑
function List({ items }) {
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
return sorted.map(item => <Item key={item.id} data={item} />);
}
// 错误:渲染阶段有副作用
function List({ items }) {
analytics.track('list_rendered'); // 可能被调用多次!
items.sort((a, b) => a.name.localeCompare(b.name)); // 修改了 props!
return items.map(item => <Item key={item.id} data={item} />);
}
8.2 外部状态的一致性
并发渲染下,同一次渲染过程中读取外部可变状态可能得到不同的值(tearing)。useSyncExternalStore 就是为解决这个问题而设计的:
import { useSyncExternalStore } from 'react';
const store = {
value: 0,
listeners: new Set(),
subscribe(listener) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
},
getSnapshot() {
return this.value;
},
setValue(v) {
this.value = v;
this.listeners.forEach(l => l());
},
};
function Counter() {
const value = useSyncExternalStore(
store.subscribe.bind(store),
store.getSnapshot,
);
return <span>{value}</span>;
}
8.3 Effect 的幂等性
由于 Strict Mode 的双重调用以及并发渲染可能导致的多次挂载,Effect 必须具备幂等性并正确清理:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData);
return () => controller.abort();
}, []);
九、总结
并发渲染是 React 架构的一次根本性演进。它并不会让渲染更快,而是让 React 更聪明地安排渲染工作——确保用户感知到的响应速度始终是最优的。