react中的setState是同步还是异步?react为什么要将其设计成异步?
壹 ❀ 引
了解react
的同学都知道,react
遵守渲染公式UI=Render(state)
,状态决定了组件UI最终渲染的样子(props
也可以理解为外部传入的状态),由此可见state
对于react
的重要性。而在实际使用中,若我们想修改状态必须得借用APIsetState
,也只有通过此方法修改状态才能顺利触发react
下次render
,那么对于一个使用如此高频的方法你了解它多少呢?
这里我们可以先抛出几个问题:
setState
是同步还是异步?- 什么情况下同步?什么情况下异步?
setState
批量合并是指只执行最后一次吗?比如执行了3次,第1,2次到底有没有执行?- 为什么要将
setState
设计成异步?这样设计的好处是什么?
设想一下,上述问题如果在面试中遇到该如何作答?那就让我们带着问题出发,另外,文中所有例子强烈建议本地跑一跑,加深对于概念的理解,那么本文开始。
贰 ❀ setState中的同步与异步
贰 ❀ 壹 updater为对象时的异步情况
setState
接受一个带有形式参数的 updater
函数(也可能直接是一个对象)与一个回调callback
(可选)。
setState(updater, [callback])
官方明确表示,setState
对于this.state
并不是立刻更新,若在调用setState
后想要立刻获取到最新的this.state
,那么建议在setState
的callback
或者声明周期componentDidUpdate
中获取,比如:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log(this.state.num);//2
}
handleOnClick = () => {
this.setState({ num: this.state.num + 1 }, () => {
console.log(this.state.num);//2
});
console.log(this.state.num);//1
}
render() {
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
其实既然官方特意强调在callback
中获取最新的this.state
,那就已经说明存在某些地方拿不到最新的this.state
的情况,比如上述代码中setState
后我们立刻读取sum
,可以发现num
还是1,那么到这里我们可以得知setState
对于this.state
的更新确实是异步。
问题来了,react
为什么将setState
设计成异步呢?设想下我们有如下这种场景:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log(this.state.num);//2
}
handleOnClick = () => {
this.setState({
num: this.state.num + 1
}, () => {
console.log(this.state.num)//2
});
this.setState({
num: this.state.num + 1
}, () => {
console.log(this.state.num)//2
});
console.log(this.state.num);//1
}
render() {
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
当点击按钮,我们需要连着两次执行setState
,那么react
会帮我们修改两次this.state
然后重新render
两次吗?很明显并不是,react
会批量合并多次setState
操作,上述例子num
最终是2,且render
在点击后只会渲染一次。
React
在开始重新渲染之前, 会有意地进行"等待",直到所有在组件的事件处理函数内调用的 setState()
都完成之后再做最终的this.state
变更,这样可以通过避免不必要的重新渲染来提升性能。
贰 ❀ 贰 updater为函数时的异步情况
突然奇想,上述代码的需求有了些许变更,我们还是在点击后执行两次setState
,但我预期最终的sum
是3,如何做到呢?别忘了前面我们对于setState
的语法介绍,本质上updater
是一个接受最新state
与最新props
并用于返回你用来更新this.state
的函数:
// 这里可以拿到最新的state与props,注意,是最新的state,而不是最新的this.state
(state, props) => stateChange
函数写法能让我们拿到立刻变更后的state
,因此我们可以来看看这个例子:
class Echo extends React.Component {
state = {
num: 1
}
componentDidUpdate() {
console.log('我是更新完成后的this.state',this.state.num);
}
handleOnClick = () => {
this.setState((state, props) => {
console.log('第一次调用,我是最新的state',state.num)
console.log('第一次调用,我是当前的this.state',this.state.num)
// 注意,这里用的是state,不是this.state
return { num: state.num + 1 };
}, () => {
console.log('第一次调用,我是调用完成后的this.state',this.state.num)
});
this.setState((state, preProps) => {
console.log('第二次调用,我是最新的state',state.num)
console.log('第二次调用,我是当前的this.state',this.state.num)
return { num: state.num + 1 };
}, () => {
console.log('第二次调用,我是调用完成后的this.state',this.state.num)
});
console.log('我用于检验异步,此时拿不到最新的this.state',this.state.num);//1
}
render() {
console.log('用于检验render了几次');
return (
<>
<div>{this.state.num}</div>
<button onClick={this.handleOnClick}>加1</button>
</>
)
}
}
请问每次setState
时的state
与this.state
是多少,更新完成后最终的this.state
是多少?render
会执行几次呢?先思考下,答案如下:
最终this.state
是3,且每次setState
中拿到的state
(注意不是this.state
)都是我们预期修改后的,而且根据调用顺序来看,虽然确实执行了多次setState
,但最终对于this.state
的修改只有一次,且render
只执行了一次,这种情况下react
依旧做了批量合并处理。
贰 ❀ 叁 批量合并是只执行最后一次setState吗?
在上述例子中,我们在setState
执行了多次this.state.num+1
的操作,但最后this.state.num
是2,那么请问,所谓批量合并,是只执行了其中某一次的setState
吗?执行的是第一次还是最后一次?其实看个例子就懂了:
class Echo extends React.Component {
state = {
a:false,
b:false
}
componentDidUpdate() {
console.log(this.state);
}
handleOnClick = () => {
this.setState({
a:true
}, () => {
console.log(this.state)
});
this.setState({
b: true
}, () => {
console.log(this.state)
});
this.setState({
a: false
}, () => {
console.log(this.state)
});
}
render() {
return (
<>
<button onClick={this.handleOnClick}>click me</button>
</>
)
}
}
事实证明,setState
在批量合并过程中还是会是执行每个setState
,但在updater
是对象的情况下,setState
对于相同key
的操作始终以最后一次修改为准:
// 执行了三次了加1,但最终其实只会加一次
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});
this.setState({num:this.state.num + 1});
比如上述代码执行了三次+1操作,等待渲染结束后,我们会发现结果num
其实只加了一个1,它等同于:
const num = this.state.num;
this.setState({num:num + 1});
this.setState({num:num + 1});
this.setState({num:num + 1});
这里的const num = this.state.num;
就相当于是一个快照,setState
确实执行了三次,只是设置的一直都是相同的值,导致最终this.state
的值确确实实是以最后一次为准。
贰 ❀ 叁 什么情况下setState是同步?
其实要回到这个问题,我们只需要知道什么情况下setState
是异步,那么反过来的情况自然就都是同步了。一般来说,react
在事件处理函数内部的 setState
都是异步的,比如合成事件onClick
,onBlur
,其次react
提供的生命周期钩子函数中也是异步。
那么是不是说只要setState
不在合成事件内调用,我们就能实现同步更新了呢?来看个例子:
class Echo extends React.Component {
state = {
num:1
}
componentDidUpdate() {
console.log(this.state.num);//2 3 4
}
handleOnClick = () => {
setTimeout(()=>{
this.setState({num:this.state.num+1});
this.setState({num:this.state.num+1});
this.setState({num:this.state.num+1});
console.log(this.state.num);//4
})
}
render() {
console.log('我在render了');// 执行3次
return (
<>
<button onClick={this.handleOnClick}>click me</button>
</>
)
}
}
事实上,超出了react
能力范畴之外的上下文,比如原生事件,定时器回调等等,在这里面进行setState
操作都会同步更新state
。比如在上述例子中,我们实现了在setState
后获取到同步更新的this.state
,但遗憾的是,react
此时并不能做到批量合并操作,导致render
执行了三次。
贰 ❀ 肆 为什么一定要设计成异步,同步批量处理不行吗?
其实在众多react
使用者中一直有一个这样的疑问,虽然我们知道异步本质目的是为了异步累积处理setState
,达到减少render
提升性能的目的。那么问题来了,异步能做到批量处理,同步难道就不行吗?我们让state
同步更新,只要在最终render
时做好把控,不是一样能达到这样的效果,而且从代码可读上来说,同步更利于状态维护。对此,官方给出了合理解释,大致分为三点:
-
保证内部的一致性
即便
setState
能做到同步,react
对于props
的更新依旧是异步,这是因为对于一个子组件而言,它只有等到父组件重新渲染了,它才知道最新的props
是多少,所以让setState
异步的另一个原因是为了让state,props,refs
更新的行为与表现保持一致。我们假设有下面这段代码,它是同步执行:console.log(this.state.value);//0 this.setState({value:this.state.value+1}); console.log(this.state.value);//1 this.setState({value:this.state.value+1}); console.log(this.state.value);//2
但现在我们有个场景,这个状态需要被多个兄弟组件使用,因此我们需要将其状态提升到父组件,以便于给多个兄弟组件共享:
console.log(this.props.value);//0 this.props.onIncrement(); console.log(this.props.value);//0 this.props.onIncrement(); console.log(this.props.value);//0
很遗憾上述代码并不能按照我们预期的执行,因为在同步模型中,
this.state
会立刻更新,但是this.props
并不会,而且在没有重新渲染父组件的情况下,我们没办法立刻更新this.props
,那要假设要做到每执行一次onIncrement
能让兄弟组件都拿到最新的props
,唯一的办法就是立刻重新渲染父组件,而这种场景下,已经与我们最初的批量合并处理减少重复渲染相违背了。而为了解决这个问题,
react
将this.props
与this.state
更新设计为异步,这也让状态提升时对于状态的管理更合理与更安全。
-
性能优化
如果
setState
是同步的话,那么对于状态的改变一定会按照setState
调用顺序来执行并改变,但事实上react
会根据setState
不同的调用源,为这些setState
分配不同的优先级,调用源包含事件处理,网络请求,动画等等。官方给了一个这样的例子,比如我们在一个聊天窗口聊天,输入的信息变化会触发
setState
,而此时我们搜到了一条新消息,新消息也会触发setState
,那么这里更好的做法是延迟新消息的setState
的执行,降低其优先级,这样就能避免输入过程中因为新消息触发的渲染,导致输入过程中抖动以及延迟。如果给某些更新分配更低的优先级,那么就可以把它们拆分成几毫秒的渲染块,这样用户也不会察觉到。 -
异步创造更多可能性
异步除了性能优化之外,异步也为未来的
react
升级埋下更多可能性。比如我们有个需要,需要从页面A导航到页面B,那么这时候你可能需要做一个加载动画,等待B页面渲染。但如果导航切换特别快,闪烁一下加载动画又会降低用户体验。而站在异步的基础上,当我们调用
setState
去渲染一个新页面,因为异步的缘故,react
可以在后台渲染这个新页面,而且不去阻塞旧页面的交互,假设等待时间过长,我们还是可以展示loading
,但如果等待耗时非常短暂,setState
可以因为异步批量合并的缘故减少渲染,不会让页面频繁闪动,从而提升用户体验。
对于问题的原回答,可阅读此issues:RFClarification: why is setState
asynchronous?那么到这里,我们站在react
官方的角度解释了为什么react
中的setState
是异步而不能是同步。
叁 ❀ 总
我们花了较大的篇幅解释了好几个setState
相关非常意思的问题,但其实我们还剩余一个问题没解释,那就是像合成事件中的setState
会异步执行批量合并操作,而像原生定时器中的setState
却不会如此。那么react
如何区分这两者情况,或者说react
在合成事件的底层到底做了什么?
考虑到篇幅的问题,这个问题我打算放在与setState
异步紧急相连的合成事件篇章去解释,也便于大家对于本篇知识点的快捷梳理与消化。请回到文章开头再次面对最初的那几个答案,那么现在你心中是否有了自己的答案?
文章最后附上非常经典的setState
点三次的问题,代码如下:
class Echo extends React.Component{
state = {
count: 0
}
// count +1
increment = () => {
console.log('increment setState前的count', this.state.count)
this.setState({
count: this.state.count + 1
});
console.log('increment setState后的count', this.state.count)
}
// count +1 三次
triple = () => {
console.log('triple setState前的count', 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后的count', this.state.count)
}
// count - 1
reduce = () => {
setTimeout(() => {
console.log('reduce setState前的count', this.state.count)
this.setState({
count: this.state.count - 1
});
console.log('reduce setState后的count', this.state.count)
}, 0);
}
render(){
return <div>
<button onClick={this.increment}> +1 </button>
<button onClick={this.triple}> +1 三次 </button>
<button onClick={this.reduce}> -1 </button>
</div>
}
}
大家可以自行思考,如果此时你已经能轻松回答,那么你对于setState
同步异步问题已经有了一个清晰的认知了。剩余的问题,我们在下一篇合成事件再详细阐述,那么到这里本文结束。