15. setState的更新是异步的吗
setState的更新是异步的?
- 最终打印结果是Hello World;
- 可见setState是异步的操作,我们并不能在执行完setState之后里面拿到最新的state的结果
- 异步执行结果如下图

- 1.3.3. 为什么setState设计为异步呢?
- setState设计为异步其实之前在GitHub上也有很多的讨论;
- React核心成员(Redux的作者)Dan Abramov也有对应的回复,有兴趣的可以参考下一下链接
- 官网地址:https://github.com/facebook/react/issues/11527#issuecomment-360199710
- 1.3.4. 我对其回答做一个简单的总结:
-
- setState设计为异步,可以显著的提升性能;
- 1.1. 如果每次调用setState都进行一次更新,那么以为意味着render函数会被频繁调用,界面重选渲染那,这样效率很低的;
- 1.2. 最好的办法应该是获取到多个更新,之后进行批量更新;
-
- 如果同步更新了state, 但是还没有执行render函数,那么state和props不能保持同步;
- 2.1. state和props不能保持一致性,会在开发中产生很多的问题;
-
- setState批量更新机制:
- 3.1.
React18之前是同步的,React18之后都是异步的 - 3.2. 这里
调用了setState三次,render函数就会执行三次,进行三次diff算法,dom会更新三次,dom更新的话会引起回流和重绘 - 3.3. 为什么用调用setState三次?
- 理想情况下可以直接调用counter + 3
- 但是在真实开发中会出现componentDidMount里面同时发送网络请求然后差不多时间段返回数据让setState执行多次进行赋值,但是更新state只要更新一次就可以了,更新只要更新一次就可以了
- 3.4 所以最好的办法:获取到多个更新,之后进行批量更新(一起做一个合并,合并完值之后调用一次render函数就可以,对DOM也是进行一次diff算法,对DOM进行一次更新)
- 3.5 React18之前有一些地方是不会做批量处理的,例如:promise的then回调不会做批处理,React18之后所有东西都是批量处理
- 3.6. 关键代码代码如下:
this.setState({counter: this.state.counter + 1 // 0 + 1})this.setState({counter: this.state.counter + 1 // 0 + 1})this.setState({counter: this.state.counter + 1 // 0 + 1})
-
- setState异步更新的原理:
- 原理:当执setState更新值操作的时候,会做一个批量处理,把要更新的东西做一个异步操作,先不更新。搞一个队列,会把更新的操作加入到队列里面,等到需要真正更新的时候,取出每次需要更新具体的内容,进行依次合并,队列是有顺序的,队列的特点就是先进先出;
- 内部源码:内部做了一个
do{} while循环,用do{} while循环把队列里面所有东西都给取出来,取出来之后挨个合并,合并完之后在执行一次render函数
-
- 上面的代码就会被合并,只会执行一次,真的会被合并吗?
- 这样来的写的话
this.state.counter + 1,this.state.counter取到的是一个值为0, 所以是:0 + 1,依然是有合并的,在构建对象的时候,就已经把counter的值取出来了,所以是0+1为1,所以相当于把下面的代码三步做了一样的操作0+1,这里还没涉及设置它 - 有点类似于obj1的操作
const obj1 = { counter: 0 + 1 } - 但是结果是为1的,每次设置值,都是0+1这个值,这个东西同时依然有合并,可以验证它的合并过程,值为1, render函数只执行了一次
- 如下图:

- 关键代码如下:
export class App extends Component {constructor() {super()this.state ={message: 'hello world',counter: 0}}increment () {console.log('-----------')this.setState({counter: this.state.counter + 1 // 0 + 1})this.setState({counter: this.state.counter + 1 // 0 + 1})this.setState({counter: this.state.counter + 1 // 0 + 1})}render() {const { message, counter } = this.stateconsole.log('render被执行')return (<div><h2>message: {message}</h2><button onClick = {e => this.changeText()}>修改文本</button><h2>当前计数:{ counter }</h2><button onClick= { e => this.increment()}>+1</button><Hello message={message} /></div>)}} -
- 其实上面这段代码刚好可以验证是异步的,如果是同步的话,第一个setState更新完之后结果应该是1,第二个setState更新完之后结果应该是2, 第三个setState更新完之后结果应该是3
但是它没有,结果依旧是1,所以间接证明了它就是异步的,render函数执行一次
- 其实上面这段代码刚好可以验证是异步的,如果是同步的话,第一个setState更新完之后结果应该是1,第二个setState更新完之后结果应该是2, 第三个setState更新完之后结果应该是3
-
- 现在合并的值依然是之前的值,如果
想要真正的被合并,传入一个回调函数
- 运行结果如下图:

- 关键代码如下:
// 现在合并的值依然是之前的值,如果想要真正的被合并,传入一个回调函数// 这里主要讨论了两个问题:// 一:就是在一个点击函数里面执行了三次setState, render函数也只会被执行一次// 二:三次里面刚好出现了递增的情况, 需要传入回调函数,在回调函数里面使用传入进来的state, 因为传入进来的state已经是合并完之后另外一个state的值(更新后的值)this.setState((state) =>{// 使用传递进来state// 这个state里面的数据更新 这个本质上是放到一个队列,队列里面放了一个个函数,到时候取出来的时候会回调这个函数,回调这个函数的时候会传入一个state,回调这个函数的时候会把最新的state传进来// 经过函数的调用的话就会做一个+1的操作,紧接着取出来第二个函数的时候,又要传入一个state,原来的state就已经+1了,所以传进来的state就是另外一个state(更新计算后的state值), 拿到最新的state进行回调// 这里拿到的this.state的值是不是正确的?// 回调这个函数的时候准备做更新了, 不是加入到队列,已经从队列里面取出来了准备开始执行了 ,这个地方this.state的值有没有更新取决于上次更新有没有把上一次this.state里的值有没有改掉还是一次性改掉// 取到的值依然是0,那就是依然没有更改,做一次性更改,等到所有东西都合并到一起之后再做一次性更新// console.log(this.state.counter)return {counter: state.counter + 1}})this.setState((state) =>{return {counter: state.counter + 1}})this.setState((state) =>{return {counter: state.counter + 1}}) - 现在合并的值依然是之前的值,如果
-
3./4./5./6./7.主要讨论了两个问题:
-
- 问题一:就是在一个点击函数里面执行了三次setState,
render函数也只会被执行一次
- 问题一:就是在一个点击函数里面执行了三次setState,
-
- 问题一:
调用三次setState里面刚好出现了递增的情况, 需要传入回调函数,在回调函数里面使用传入进来的state, 因为传入进来的state已经是合并完之后另外一个state的值(更新后的值)
- 问题一:
-
-
1.4. 如何获取异步的结果
-
- 那么如何可以获取更新后的值呢?
-
- 方式一: setState的回调
- 2.1. setState接受两个参数,第二个参数是一个回调函数,这个回调函数会在更新后执行;
- 2.2. 格式如下:
setState(partialState, callback)

-
- 方式二: 也可以在生命周期函数:
componentDidUpdate(prevProps, prevState, snapshot) { console.log(this.state.message);}
-
-
1.5. setState一定是异步吗?(React18之前)
-
- 验证一:setTimeout中的更新
setTimeout(() => {// 在React18之前,setTimeout中setState操作,是同步操作// 在React18之后,setTimeout中setState是异步操作(批处理)this.setState({ message: '你好啊,李银河' })console.log(this.state.message) // React18之前`你好啊,李银河` React18之后`hello World`}, 0) -
- 验证二:原生DOM事件
componentDidMount() {const btnEl = document.getElementById('btn');btnEl.addEventListener('click', () => {this.setState({ message: '你好啊,李银河' }) // React18之前`你好啊,李银河`})}-
- 其实分层两种情况:
-
- 在组件生命周期或React合成事件(React事件回调), setState是异步;
-
- 在setTimeou或者原生dom事件中,setState是同步;
-
-
1.6. setState一定是异步吗?(React18之后)
-
- 在React18之后,默认所有的操作都被放到了批处理中(异步处理)
- 官网截图:

-
- 如果希望代码可以同步拿到,则需要执行特殊的flushSync操作;
// 导入flushSyncimport { flushSync } from 'react-dom'// 使用flushSync,传入一个回调函数flushSync(() => {this.setState({ message: '你好啊,李银河' })// 这里的代码依然是批处理,这个回调里面的函数里的代码都是当成一次更新console.log('flushSync====',this.state.message) // hello world})console.log(this.state.message) // `你好啊,李银河`
-
