相信大家对于react的setState肯定是不陌生了, 这是一个用于更新状态的函数. 但是在之前有一道非常经典的面试题就是关于setState是同步还是异步的问题, 具体可以参考我之前写的一篇文章: 一篇文章彻底理解setState是同步还是异步!. 对于react 18之前的版本, 上文说的东西确实没错, 但是react团队已经在18中对批处理的行为做了更改, 会尽可能的将所有能进行批处理的内容都进行批处理, 以获取更好的性能, 今天我们就来聊聊react 18中对这一行为做了哪些更改.
先看现象, 再说结论
我们都用下面这段代码在不同版本的react上进行测试
class App extends React.Component {
state = {
data: 1
}
test = () => {
setTimeout(() => {
this.setState({data: 2});
console.log('data', this.state.data);
this.setState({data: 3});
console.log('data', this.state.data);
}, 0);
}
render() {
console.log("render");
return (
<div>
<button onClick={this.test}>{this.state.data}</button>
</div>
)
}
}
大家觉得输出的data值会是什么, 这个render又会打印几次?
在react 17.x下, 我们能够同步的获取到data的值, 所以输出会是2, 3.同时也会经历两次react的整个render过程, 所以也会导致render被打印两次, 这都是因为setTimeout带来的影响, 具体的解释可以参考之前的文章.

而在最新版的react 18上, 这两个setData也会被异步的批处理, 合并为一次进行更新, 所以我们拿到的值始终是1, render函数也只会被打印一次.

react 18做了什么修改
官方的说明在这里: https://github.com/reactwg/react-18/discussions/21
总结一下就是:
从react 18开始, 使用了createRoot创建应用后, 所有的更新都会自动进行批处理(也就是异步合并).使用render的应用会保持之前的行为.
如果你想保持同步更新行为, 可以使用ReactDOM.flushSync().
// 新版
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
const container = document.getElementById('root');
// Create a root.
const root = ReactDOM.createRoot(container);
// Render the top component to the root.
root.render(<App />);
// 旧版
// import React from 'react';
// import ReactDOM from 'react-dom';
// import './index.css';
// import App from './App';
// import reportWebVitals from './reportWebVitals';
// ReactDOM.render(
// <React.StrictMode>
// <App />
// </React.StrictMode>,
// document.getElementById('root')
// );
// // If you want to start measuring performance in your app, pass a function
// // to log results (for example: reportWebVitals(console.log))
// // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
// reportWebVitals();
至于为什么会这样, 我们直接从源码入手就好了.
我们知道, react的setState最终会走到scheduleUpdateOnFiber来进行更新, 之前最关键的一段代码就在这里
// react 17.x
if (executionContext === NoContext) {
// Flush the synchronous work now, unless we're already working or inside
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
// scheduleCallbackForFiber to preserve the ability to schedule a callback
// without immediately flushing it. We o