结论
React
的 setState
到底是异步还是同步?先给出结论:
- setState 在 React 事件处理函数中或 React 生命周期方法中是异步
- 在 setTimeout , Promise 等异步方法中或原生事件中是同步
注意以上是在React 18
以前的版本处理方式,18 开始使用createRoot
的 api 的话,就不会有这种问题了,就算是setTimeout
里的代码也能异步批量执行(同样,使用Hooks
的useState
也不会有这种问题)。下文也是基于 React 18 前的版本class
组件进行分析。
代码解析
如下代码片段:
class App extends React.Component {
constructor(){
super()
this.state = {
count: 0
}
}
increment = () => {
console.log('increment setState前', this.state.count)
this.setState({
count: this.state.count + 1
})
console.log('increment setState后', this.state.count)
}
triple = () => {
console.log('triple setState前', this.state.count)
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
this.setState({
count: this.state.count + 1
})
console.log('triple setState后', this.state.count)
}
reduce = () => {
setTimeout(()=>{
console.log('reduce setState前', this.state.count)
this.setState({
count: this.state.count - 1
})
console.log('reduce setState后', this.state.count)
}, 0)
}
render(){
return(
<div>
<button onClick={this.increment}>点击增加</button>
<button onClick={this.triple}>点击三倍增加</button>
<button onClick={this.reduce}>点击减少</button>
</div>
)
}
}
依次点击后,控制台打印结果:
increment setState前 0
increment setState后 0
triple setState前 1
triple setState后 1
reduce setState前 2
reduce setState后 1 // React 18使用 createRoot 此处会打印: reduce setState后 2
从打印信息及其顺序可能会有疑惑,increment
、triple
两个函数 setState
是个异步操作,点击后state
并不会立即发生改变,但reduce
是怎么回事,setState
又成了同步更新了?
批量更新
带着这个疑问,我们来看一下setState
的执行流程:
从上图可以看出,每次setState
都会执行一次这个流程,消耗就会很大,类似如下流程:
this.setState({
count:thisstate.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})
this.setState({
count:this.state.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})
this.setState({
count:this.state.count+1 // ===> shouldComponentUpdate ===> componentWillUpdate ===> render ===> componentDidUpdate
})
其实不然,React
的 setState
采用的是批量更新的机制,每进行一次setState
就会把它塞进一个队列,等“时机成熟”,再把队列内积攒的state
结果做合并,最后只针对最新的state
值走一次更新流程,类似Vue
的nextTick
和浏览器的EventLoop
。如下:
this.setState({
count:thisStatecount+1 // ===> 入队,[count+l的任务]
})
this.setState({
count:this.state.count+1 // ===> 入队,[cont+1的任务,count+l的任务]
})
this.setState({
count:thisstate.count+1 // ===> [count+1的任务,count+1的任务,count+l的任务,合并state,[count+1的任务]
})
// ⇊
// 合并state,[count+1的任务]
// ⇊
// 执行count+1的任务
所以,只要同步队列没结束,就会一直这么“攒”下去。所以即使像下面这样setState
100次,也只是入队列的次数在增加,并不会执行100次render
,当100次入队列任务结束后,只是队列的内容发生变化,state
本身并不会立刻改变。
test=()=>{
console.log('循环100次setState前的count', this.state.count)
for(leti=0; i<100; i++){
this.setState({
count: this.state.count+1
})
}
console.log('循环100次setState后的count', this.state.count)
}
// 循环100次setState前的count 0
// 循环100次setState前的count 0
同步背后的现象浅析
并不是
setTimeout
改变了setState
,而是setTimeout
帮助setState
“逃脱”了React
对它的管控只要是在React
管控下的setState
,一定是异步的。
在 React
的 setState
函数实现中,会根据一个变量 isBatchingUpdates
判断是直接更新this.state
还是放到一个 updateQueue
中延时更新。
而 isBatchingUpdates
默认是 false
,表示 setState
会同步更新 this.state
。
但是,有一个函数 batchedUpdates
,该函数会把 isBatchingUpdates
修改为 true
。而当 React
在调用事件处理函数之前就会先调用这个 batchedUpdates
将isBatchingUpdates
修改为true
。这样由 React
控制的事件处理过程 setState
不会同步更新 this.state
,而是异步的。
所以说setState
本身是同步的,一旦走了react
内部的合并逻辑,放入了updateQueue
队列中就变成异步了。
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 chaoyumail@126.com