一起学习React18新特性-自动批处理(Automatic Batching)
Yakim Zhang

在 React 18 之前,React 就已经对 state 更新进行批处理了,仅支持浏览器事件的批处理,但是不包含 Promise,setTimeout,native event handlers。本次更新,对批处理进行了改进,所有状态都会启动批处理。

先来看看在 React17 中进行的批处理

下面的代码,当点击 Click Me! 执行 handleOnClick()函数后,会将 2 个状态更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import { useState } from "react";
import "./App.css";
const App = () => {
const [additionCount, setAdditionCount] = useState(0);
const [subtractionCount, setSubtractionCount] = useState(0);

console.log("Component Rendering");

const handleOnClick = () => {
setAdditionCount(additionCount + 1);
setSubtractionCount(subtractionCount - 1);
};

return (
<div>

<button
style={{ width: "50%", height: "30%" }}
onClick={() => {
handleOnClick();
}}
>
Click Me!
</button>
<div>Add Count: {additionCount}</div><div>Substraction Count: {substractionCount}</div>
</div>
);
};
export default App;

注意看控制台,console.log(“Component Rendering”) 只打印了一次,说明只发生了一次更新。

这说明 react 已经把 2 次 setState 合并成了一次,进行了批处理。
输出

再来看看 React17 不能进行批处理的情况

当遇到非浏览器事件时,批处理就无效了,下面的代码以 fetch 异步事件为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 异步事件
const handleOnClickAsync = () => {
fetch(“https://jsonplaceholder.typicode.com/todos/1").then(() => {
setAdditionCount(additionCount + 1);
setSubstractionCount(substractionCount — 1);
});
};

// html
<button style={{ width:50%”, height:30%” }}
onClick ={() => {
handleOnClickAsync();
}}
>
Click Me Async Call!
</button>

可以看到控制台中,输出了 2 次,说明页面刷新了 2 次,react 没有进行合并批处理。
输出

这样会有什么问题呢

对于小应用来说,重新渲染不会产生重大影响。但是,随着对于大型项目,嵌套组件的数量会很多。因此,如果父组件执行状态更新,则整个组件树将在每次状态更新时重新渲染,这样会拖慢应用的速度。

React 18 改进了批处理

在 React v18 中从任何位置调用的状态更新将默认进行批处理。这将批处理状态更新,包括浏览器事件处理程序、异步操作、超时和间隔。

下面看一个案例,安装最新的 react 版本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";
const App = () => {
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);
console.log("React 18 Application Re-Rendering");
// 点击事件
const handleClick = () => {
// 1 Re-Render
setClicked(!clicked);
setCount(count + 1);
};

// 异步
const handleAsyncClick = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1").then(() => {
// trigger 1 re-render due to React 18 Improved Batching
setClicked(!clicked);
setCount(count + 1);
});
};

// timeout/interval
const handleTimeOutClick = () => {
setTimeout(() => {
// trigger 1 re-render due to React 18 Improved Batching
setClicked(!clicked);
setCount(count + 1);
});
};

return (
<div className="App">
{" "}
<header className="App-header">
<div> Count: {count} </div><div> Clicked: {clicked} </div>
<button onClick={handleClick}> Event Handler </button>{" "}
<button onClick={handleAsyncClick}> Async Handler </button>
<button onClick={handleTimeOutClick}> Timeout Handler </button>
</header>

</div>
);
};

export default App;

上面的代码,每个事件处理程序中都会发生两个状态更新。
分别点击三个按钮,可以看到在浏览器控制台中打印三个日志,而不是 6 个日志,说明 react 进行了合并批处理。
输出

阻止批处理

某些情况下不能使用批处理,例如第二个状态的更新依赖第一个状态的结果。

react 提供了 flushSync 来实现阻止批处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { flushSync } from "react-dom";

const App = () => {
console.log("update");
const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);

const handleClick = () => {
flushSync(() => {
setCount(count + 1);
});
setClicked(true);
};

return (
<>
<div> Count1: {count} </div>
<button onClick={handleClick}> Event Handler </button>{" "}
</>
);
};

可以看到,当点击 Event Handler 时,输出了 2 次 update,说明 flushSync 中的状态更新被单独执行了,没有合并。