React 18 新特性与并发渲染
React 18 的核心更新:自动批处理、Transitions、Suspense 改进和并发渲染机制
问题
React 18 带来了哪些新特性?并发渲染是如何实现的?
解答
自动批处理(Automatic Batching)
React 18 之前,只有 React 事件处理函数内的状态更新会被批处理。React 18 扩展了批处理的范围,Promise、setTimeout、原生事件处理函数中的更新也会自动合并:
// React 18 之前:不会批处理
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 会触发两次渲染
}, 1000);
// React 18:自动批处理
setTimeout(() => {
setCount(c => c + 1);
setFlag(f => !f);
// 只触发一次渲染
}, 1000);
如果需要退出自动批处理,可以使用 flushSync:
import { flushSync } from 'react-dom';
function handleClick() {
flushSync(() => {
setCounter(c => c + 1);
});
// DOM 已更新
flushSync(() => {
setFlag(f => !f);
});
// DOM 再次更新
}
Transitions
Transitions 用于区分紧急更新和非紧急更新。紧急更新包括用户输入、点击等需要立即响应的操作;非紧急更新如 UI 过渡动画等可以稍后处理。
使用 startTransition
import { startTransition } from 'react';
function handleChange(input) {
// 紧急:更新输入框
setInputValue(input);
// 非紧急:更新搜索结果
startTransition(() => {
setSearchResults(input);
});
}
使用 useTransition
import { useTransition } from 'react';
function SearchPage() {
const [isPending, startTransition] = useTransition();
function handleChange(input) {
setInputValue(input);
startTransition(() => {
setSearchResults(input);
});
}
return (
<>
<input onChange={e => handleChange(e.target.value)} />
{isPending && <Spinner />}
<Results data={searchResults} />
</>
);
}
Suspense 改进
React 18 中的 Suspense 基于并发渲染实现,行为有两个重要变化:
1. 兄弟组件的挂载时机
<Suspense fallback={<Loading />}>
<ComponentThatSuspends />
<Sibling />
</Suspense>
React 18 之前,Sibling 会立即挂载到 DOM 并触发 effects,然后被隐藏。React 18 中,Sibling 不会挂载,直到 ComponentThatSuspends 完成加载。
2. ref 的指向时机
<Suspense fallback={<Loading />}>
<div ref={refPassedFromParent}>
<ComponentThatSuspends />
</div>
</Suspense>
React 18 中,refPassedFromParent.current 在 Suspense 解除锁定前保持为 null,而不是立即指向 DOM 节点。
SSR 中的 Suspense
React 18 的 Suspense 支持流式 SSR:
- 服务器可以先发送 HTML,被 Suspense 包裹的组件用 fallback 占位
- 组件数据准备好后,通过同一个流发送剩余 HTML
- 客户端可以逐步进行 hydration,不需要等待所有 JS 加载完成
- React 会优先对用户交互的区域进行 hydration
新的 API
客户端渲染
// React 18 之前
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
// React 18
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
// hydration
import { hydrateRoot } from 'react-dom/client';
hydrateRoot(document.getElementById('root'), <App />);
服务端渲染
renderToPipeableStream:用于 Node 环境renderToReadableStream:用于 Deno、Cloudflare Workers 等现代运行时
新的 Hooks
useTransition
见上文 Transitions 部分。
useDeferredValue
用于标记低优先级的变量:
import { useDeferredValue } from 'react';
function SearchPage({ input }) {
const deferredInput = useDeferredValue(input);
// input 变化时,deferredInput 会先返回旧值
// 如果没有更紧急的更新,才会更新为新值
return <Results query={deferredInput} />;
}
其他 Hooks
useId:生成服务端和客户端一致的唯一 IDuseSyncExternalStore:用于第三方状态库与并发渲染的数据同步useInsertionEffect:用于 CSS-in-JS 库优化样式注入性能
并发渲染实现原理
问题背景
React 15 中,状态更新触发的渲染无法中断。如果组件树很大,遍历和 diff 计算会长时间占用主线程,导致页面卡顿。
解决思路
- 将渲染任务拆分成可中断的小任务
- 每个任务执行时间控制在 5ms 左右
- 超时后暂停,将主线程交给浏览器处理 UI 渲染和用户交互
- 在后续帧中继续执行未完成的任务
- 为任务分配优先级,优先执行高优任务
Fiber 架构
Fiber 是一种链表数据结构,每个 Fiber 节点包含:
{
stateNode, // 组件实例或 DOM 元素
child, // 子节点
sibling, // 兄弟节点
return, // 父节点
alternate, // 连接 current 树和 workInProgress 树
// ...
}
React 维护两棵 Fiber 树:
- Current Fiber 树:当前屏幕显示的内容
- workInProgress Fiber 树:正在内存中构建的新树
两棵树通过 alternate 属性连接,构建完成后切换指针即可完成更新。
渲染流程
渲染分为两个阶段:
- Render 阶段(可中断):遍历 Fiber 树,计算变化,更新 workInProgress 树
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
- Commit 阶段(不可中断):将 workInProgress 树作为 current 树,更新 DOM
时间切片
shouldYield 函数判断是否需要中断:
function shouldYieldToHost() {
const timeElapsed = getCurrentTime() - startTime;
if (timeElapsed < 5) {
// 执行时间小于 5ms,继续执行
return false;
}
// 执行时间过长,检查是否有高优任务
// 如用户输入、绘制等
return true;
}
中断后,React 使用 MessageChannel 创建宏任务,在后续帧中继续执行:
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = () => {
// 继续执行未完成的任务
workLoop();
};
// 中断时发送消息
port.postMessage(null);
优先级控制
React 18 使用 Lanes 模型管理优先级,不同更新对应不同的 Lane:
SyncLane:同步更新,最高优先级(如用户输入)DefaultLane:默认优先级TransitionLane:过渡更新,低优先级RetryLane:重试更新,最低优先级
当高优先级更新到来时,会中断低优先级更新,优先处理高优任务。
升级指南
1. 使用新的根节点 API
// 旧版本仍然兼容,但只有使用 createRoot 才能启用新特性
import { createRoot } from 'react-dom/client';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
2. TypeScript 类型更新
需要显式声明 children 类型:
interface MyButtonProps {
color: string;
children?: React.ReactNode;
}
3. 不再支持 IE
React 18 放弃了对 Internet Explorer 的支持。
关键点
- 自动批处理扩展到 Promise、setTimeout 等异步操作中,减少不必要的渲染
- Transitions 通过
startTransition和useTransition区分紧急和非紧急更新,提升交互响应速度 - Suspense 在 React 18 中基于并发渲染重新实现,支持流式 SSR 和逐步 hydration
- Fiber 架构通过双缓存机制和链表结构实现可中断的渲染,每个任务执行时间控制在 5ms 左右
- Lanes 优先级模型允许高优任务中断低优任务,确保用户交互的及时响应
目录