# 1.自动批处理
批处理是指 React 将多个状态更新合并为单个重新渲染,以提高性能。
在 React 17 及之前版本中,只在 React事件处理函数 中进行批处理更新,而在其他情况下(如 promise、setTimeout、原生事件处理程序等)则不会批处理。
举个例子,React 18 之前,渲染次数和更新次数是一样的,即使我们更新了两个状态,每次更新组件也只渲染一次。(代码如下)
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<button
onClick={() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
// 在React事件中被批处理
}}
>
{`count1 is ${count1}, count2 is ${count2}`}
</button>
);
};
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
但是,如果我们把状态的更新放在 promise
或者 setTimeout
里面:
每次点击更新两个状态,组件都会渲染两次,不会进行批量更新。(代码如下)
import React, { useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={() => {
setTimeout(() => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在 setTimeout 中不会进行批处理
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在原生js事件中,情况也是一样,点击更新两个状态,组件都会渲染两次,不会进行批量更新。(代码如下)
import React, { useEffect, useState } from 'react';
// React 18 之前
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
useEffect(() => {
document.body.addEventListener('click', () => {
setCount1(count => count + 1);
setCount2(count => count + 1);
});
// 在原生js事件中不会进行批处理
}, []);
return (
<>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</>
);
};
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
TIP
在 React 18 上面的三个例子只会有一次 render,因为所有的更新都将自动批处理。这样无疑是很好的提高了应用的整体性能。
# 不过这种情况会在 React 18 中执行两次 render:
await
会破坏自动批处理:任何异步操作(await/Promise/then)都会打断批处理
import React, { useState } from 'react';
// React 18
const App: React.FC = () => {
console.log('App组件渲染了!');
const [count1, setCount1] = useState(0);
const [count2, setCount2] = useState(0);
return (
<div
onClick={async () => {
await setCount1(count => count + 1);
setCount2(count => count + 1);
}}
>
<div>count1: {count1}</div>
<div>count2: {count2}</div>
</div>
);
};
export default App;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 注意:flushSync 函数内部的多个 setState 仍然为批量更新,这样可以精准控制哪些不需要的批量更新。
- 主要优势
- 减少不必要的渲染:合并多个状态更新为单个重新渲染
- 提高性能:减少组件渲染次数,特别是对于复杂应用
- 更一致的更新行为:无论在什么环境下触发更新,行为都一致
# 2.并发模式(Concurrent Mode)
并发模式是 React 的一种新渲染机制,它允许 React 同时准备多个版本的 UI,并根据优先级选择性地渲染。这与传统的同步渲染(一次完成整个树更新)有本质区别。
React 17 和 React 18 的区别就是:从同步不可中断更新变成了异步可中断更新。
# 核心概念
- 并发渲染(Concurrent Rendering)
- 可中断渲染:React 可以在渲染过程中暂停、继续或放弃工作
- 优先级调度:区分高优先级(如用户输入)和低优先级更新(如数据加载)
- 时间切片:将渲染工作分成小块,避免长时间阻塞主线程
- 并发特性(Concurrent Features)
React 18 提供了一系列基于并发模式的新API:
- useTransition:标记非紧急更新
- useDeferredValue:延迟更新某些值
<Suspense>
:更好地控制加载状态
# 实际应用场景
- 输入响应优化:确保输入框即时响应,同时后台处理复杂计算
- 路由过渡:切换路由时保持当前界面响应,后台加载新路由
- 大数据渲染:分批渲染大型列表,避免界面冻结
- 依赖加载:优雅处理代码分割和动态导入
# 启用并发模式
从React 18开始,默认启用部分并发特性,完整启用方式:
import { createRoot } from 'react-dom/client';
// 替换原来的ReactDOM.render
createRoot(document.getElementById('root')).render(<App />);
2
3
4
# 3.关于 React 组件的返回值
在 React 17 中,如果你需要返回一个空组件,React只允许返回null。如果你显式的返回了 undefined,控制台则会在运行时抛出一个错误。
在 React 18 中,不再检查因返回 undefined 而导致崩溃。既能返回 null,也能返回 undefined(但是 React 18 的dts文件还是会检查,只允许返回 null,你可以忽略这个类型错误)。
# 4.新的 Hook
# useId
useId 是一个新的Hook,用于生成在客户端和服务端两侧都独一无二的 id,避免激活后两侧内容不匹配。它主要用于需要唯一 id 的,具有集成 API 的组件库。
这个更新不仅解决了一个在 React 17 及更低版本中的存在的问题,而且它会在 React 18 中发挥更重要的作用,因为新的流式服务端渲染响应 HTML 的方式将是无序的,需要独一无二的 id 作为索引。
# useDeferredValue
useDeferredValue 允许推迟渲染树的非紧急更新。这和防抖操作非常相似,但是有一些改进。它没有固定的延迟时间,React 会在第一次渲染在屏幕上出现后立即尝试延迟渲染。延迟渲染是可中断的,它不会阻塞用户输入。
# useSyncExternalStore
useSyncExternalStore 是一个新的 Hook,允许使用第三方状态管理来支持并发模式,并且能通过对 store 进行强制更新实现数据同步。对第三方数据源的订阅能力的实现上,消除了对 useEffect 的依赖,推荐任何 React 相关的第三方状态管理库使用这个新特性。
← React进阶 Vue和React对比 →