React 18 并发特性与新功能
并发渲染
什么是并发渲染?
并发渲染(Concurrent Rendering)是 React 18 最重要的特性。它允许 React 在渲染过程中被中断,让浏览器能够处理其他任务,从而保持应用的响应性。
在 React 17 中,一旦开始渲染,整个过程是不可中断的。如果组件树很大,用户就会感受到卡顿。而 React 18 的并发渲染可以将渲染工作分解成小块,在浏览器需要处理用户交互时暂停渲染。
自动批处理(Automatic Batching)
React 18 扩展了批处理的范围。在 React 17 中,只有在 React 事件处理程序中的多个状态更新才会被批处理。
React 17 的行为:
1
2
3
4
5
6
7
8
9
10
11
12
13
// 这些会被批处理
function handleClick() {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会重新渲染一次
}
// 这些不会被批处理
fetch('/api/data').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 会渲染两次
});
React 18 的改进:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 现在这些也会被批处理!
fetch('/api/data').then(() => {
setCount(c => c + 1);
setFlag(f => !f);
// React 只会重新渲染一次
});
// 如果你想要同步更新,可以使用 flushSync
import { flushSync } from 'react-dom';
flushSync(() => {
setCount(c => c + 1);
});
// DOM 已经更新
flushSync(() => {
setFlag(f => !f);
});
// DOM 再次更新
新的 Root API
React 18 引入了新的 createRoot
API 来替代 ReactDOM.render
。
React 17 的方式:
1
2
3
4
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
React 18 的方式:
1
2
3
4
5
import { createRoot } from 'react-dom/client';
import App from './App';
const root = createRoot(document.getElementById('root'));
root.render(<App />);
使用新的 Root API 是启用并发特性的前提。如果继续使用旧的 ReactDOM.render
,React 18 会以传统模式运行,不会启用并发特性。
Suspense 的改进
服务端渲染中的 Suspense
React 18 在服务端渲染中增强了 Suspense 的功能。现在可以在服务端使用 Suspense 来包装尚未准备好的组件。
1
2
3
4
5
6
7
8
9
import { Suspense } from 'react';
function App() {
return (
<Suspense fallback={<div>Loading comments...</div>}>
<Comments />
</Suspense>
);
}
流式 SSR
React 18 引入了流式服务端渲染,可以更早地发送 HTML 给客户端:
1
2
3
4
5
6
7
8
9
10
11
// 服务端
import { renderToPipeableStream } from 'react-dom/server';
app.use('/', (request, response) => {
const { pipe } = renderToPipeableStream(<App />, {
onShellReady() {
response.setHeader('content-type', 'text/html');
pipe(response);
}
});
});
新的 Hooks
useId
useId
用于生成唯一的 ID,在服务端渲染中特别有用:
1
2
3
4
5
6
7
8
9
10
11
12
import { useId } from 'react';
function Form() {
const id = useId();
return (
<div>
<label htmlFor={id}>姓名:</label>
<input id={id} type="text" />
</div>
);
}
useTransition
useTransition
让你可以将状态更新标记为过渡,React 会优先处理更紧急的更新:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useTransition, useState } from 'react';
function SearchPage() {
const [isPending, startTransition] = useTransition();
const [searchTerm, setSearchTerm] = useState('');
function handleChange(e) {
startTransition(() => {
// 这个更新的优先级较低
setSearchTerm(e.target.value);
});
}
return (
<div>
<input onChange={handleChange} />
{isPending && <div>搜索中...</div>}
<SearchResults query={searchTerm} />
</div>
);
}
useDeferredValue
useDeferredValue
让你可以延迟更新 UI 的非关键部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { useDeferredValue, useState } from 'react';
function SearchPage() {
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
return (
<div>
<input
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<SearchResults query={deferredSearchTerm} />
</div>
);
}
严格模式的变化
React 18 的严格模式在开发环境中会故意双重挂载组件,以帮助发现副作用问题。这意味着 useEffect
、useState
的初始化函数等可能会被调用两次。 可以通过在组件树的任何位置使用
1
2
3
4
5
6
7
8
9
10
11
12
function MyComponent() {
useEffect(() => {
// 在严格模式下,这个 effect 会运行两次
console.log('组件挂载');
return () => {
console.log('组件卸载');
};
}, []);
return <div>我的组件</div>;
}
这个变化帮助开发者编写更加健壮的代码,确保组件能够正确处理重新挂载的情况。
升级建议
从 React 17 升级到 React 18 相对简单:
- 安装 React 18
1
npm install react@18 react-dom@18
- 更新 Root API
1 2 3 4 5 6
// 旧的方式 ReactDOM.render(<App />, container); // 新的方式 const root = createRoot(container); root.render(<App />);
- 更新类型定义(如果使用 TypeScript)
1
npm install @types/react@18 @types/react-dom@18
性能提升
React 18 在性能方面有显著提升:
- 并发特性让应用在处理大量数据时保持响应
- 自动批处理减少了不必要的重新渲染
- Suspense 改进提供了更好的加载体验
- 流式 SSR改善了首屏加载时间