React 18 并发特性
React 18 的核心升级不是新增了几个 API,而是把渲染从「同步阻塞」变成「可调度」——render 可以暂停、恢复、丢弃、重试、插队。所有并发特性都为同一个目标服务:用户交互优先,页面更流畅,避免主线程长时间阻塞。
一、Concurrent:底层调度能力
并发不是「多线程」,而是一套底层渲染机制:渲染过程可中断、可恢复、有优先级。
React 18 之前渲染是同步的,一旦开始必须执行完。组件树很大时(如 render 耗时 50ms),浏览器就会卡顿。并发模式下,React 借助 Fiber + 时间切片把渲染拆成多个小任务,浏览器有空再继续,做到「边干活边响应用户」。
| 能力 | 说明 |
|---|---|
| 可中断 | render 可以暂停 |
| 可恢复 | 下次继续 render |
| 优先级 | 用户输入优先 |
| 可丢弃 | 旧 render 不一定提交 |
启用方式是用新的 createRoot:
import { createRoot } from 'react-dom/client'
function App() {
return <h1>Hello React 18</h1>
}
const root = createRoot(document.getElementById('root')!)
root.render(<App />)二、Automatic Batching:自动批处理
批处理就是把多个 state 更新合并成一次重新渲染。
React 18 之前,只有 React 事件处理函数内的更新会被批处理;Promise、setTimeout、原生事件回调里的更新不会,会触发多次渲染。React 18 把批处理扩大到所有场景:
function Counter() {
const [count, setCount] = React.useState(0)
const [flag, setFlag] = React.useState(false)
const handleClick = () => {
setTimeout(() => {
// React 18 之前:渲染 2 次;React 18:合并成 1 次
setCount((c) => c + 1)
setFlag((f) => !f)
}, 0)
}
return <button onClick={handleClick}>{count}</button>
}需要强制同步更新(如更新后立刻读取 DOM 布局)时,用 flushSync 退出批处理:
import { flushSync } from 'react-dom'
function Demo() {
const [count, setCount] = React.useState(0)
const handleClick = () => {
// 包在 flushSync 里的更新会立即同步刷新 DOM
flushSync(() => setCount((c) => c + 1))
// 此处可以读到更新后的 DOM
}
return <button onClick={handleClick}>{count}</button>
}三、Transition:过渡更新
useTransition 用来标记非紧急更新,告诉 React 这个更新可以被打断,让紧急更新(如输入框输入)优先。
典型场景是输入框搜索:输入要立即响应,但结果列表渲染可以延迟。
import { useState, useTransition } from 'react'
function Search() {
const [input, setInput] = useState('')
const [list, setList] = useState<string[]>([])
const [isPending, startTransition] = useTransition()
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setInput(e.target.value) // 紧急:输入框立即更新
startTransition(() => {
// 非紧急:可被打断,列表慢一点无所谓
setList(filterHugeList(e.target.value))
})
}
return (
<div>
<input value={input} onChange={onChange} />
{isPending && <span>加载中...</span>}
<ul>{list.map((item) => <li key={item}>{item}</li>)}</ul>
</div>
)
}useTransition 返回 [isPending, startTransition]:isPending 表示是否过渡中(可显示加载态),startTransition 开启低优先级更新。
四、useTransition vs useDeferredValue
两者都把更新降级为非紧急,核心区别在控制点不同:
| 对比 | useTransition | useDeferredValue |
|---|---|---|
| 控制对象 | 更新函数(setState) | 某个值 |
| 用法 | startTransition(() => setX(v)) | useDeferredValue(value) |
| 适用 | 改得了更新源头 | 值来自 props/父组件/第三方,改不了源头 |
import { useDeferredValue, useMemo } from 'react'
// 值是别人传进来的,控制不了源头 → 用 useDeferredValue
function List({ query }: { query: string }) {
const deferredQuery = useDeferredValue(query)
// query 立即更新,deferredQuery 延迟更新,所以输入不卡
const items = useMemo(() => filterHugeList(deferredQuery), [deferredQuery])
return <ul>{items.map((i) => <li key={i}>{i}</li>)}</ul>
}一句话:改得了更新逻辑用 useTransition,只拿得到值用 useDeferredValue。
五、Suspense 改进
Suspense 不是 18 才有(16.6 就能用于 React.lazy),但 18 增强了它,支持SSR 流式渲染 + 选择性 hydration。
import { Suspense, lazy } from 'react'
const SlowComponent = lazy(() => import('./SlowComponent'))
function Page() {
return (
<Suspense fallback={<div>加载中...</div>}>
<SlowComponent />
</Suspense>
)
}关键改进:服务端可以先发送已就绪的部分 HTML,慢的部分用 Suspense 占位,后续再流式补上,不必等整个页面就绪。
六、Tearing(撕裂)与 useSyncExternalStore
撕裂指同一次渲染中,不同组件读到了同一个外部数据源的不同值,导致 UI 不一致。
为什么并发模式才有这问题:同步渲染一气呵成,渲染期间外部数据不会变。但并发渲染可以中断:
React 自己的 state(useState)不会撕裂——React 保证一致性。问题只出在外部可变数据源(Redux 早期实现、Zustand、自写全局 store)。
useSyncExternalStore 就是为解决撕裂而生,专门订阅外部 store:
import { useSyncExternalStore } from 'react'
function useStore(store: any) {
return useSyncExternalStore(
store.subscribe, // 订阅函数,store 变化时触发回调
store.getSnapshot, // 读取当前值
store.getServerSnapshot // SSR 时的值(可选)
)
}它的保证:并发渲染中若检测到外部 store 中途变了,React 会放弃这次渲染、同步重渲染,强制所有组件读到同一个值。Redux、Zustand 在 React 18 都改用了这个 API,把一致性交给 React。
七、四个特性的关系
它们是一套体系,Concurrent 是底层能力,其余建立在它之上:
| 问题 | 对应方案 |
|---|---|
| render 中断 | Concurrent |
| 更新优先级 | Transition |
| 异步协调 | Suspense |
| 更新合并 | Automatic Batching |
| 外部状态一致性 | useSyncExternalStore |
八、常见追问
为什么 startTransition 里不能放异步逻辑? 回调必须是同步的 setState。如果里面 await,await 之后的更新就脱离了 transition 上下文,不再被标记为非紧急(React 19 用 Actions 改善了这点)。
Transition 会丢更新吗? 不会。被打断只是延后,最终一定执行完。
自动批处理如何退出? 用 flushSync(() => setState(...)),包在里面的更新立即同步刷新 DOM。
核心一句话:React 18 让 render 从「一旦开始不可中断」升级为「可暂停、可恢复、可丢弃、可插队」。Concurrent 是底层调度能力,Transition 控制优先级、Suspense 协调异步、Automatic Batching 合并更新、useSyncExternalStore 保证外部状态一致——整套设计都是为了「用户交互优先、页面更流畅」。