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
同步异步问题已经有了一个清晰的认知了。剩余的问题,我们在下一篇合成事件再详细阐述,那么到这里本文结束。
参考
React 中setState更新state何时同步何时异步?
react中的setState是同步还是异步?react为什么要将其设计成异步?的更多相关文章
- React中this.setState是同步还是异步?为什么要设计成异步?
在使用react的时候,this.setState为什么是异步呢? 一直以来没有深思这个问题.昨天就此问题搜索了一下. react创始人之一 Dan Abramovgaearon在GitHub上回答了 ...
- vue为什么要设计成异步队列渲染
异步队列渲染 上一篇文章是在vue2.0 中通过Object.defineProperty去拦截并监听数据变化的响应式原理,这篇文章将会沿着图谱继续深入探索,在依赖被通知变化了之后,会触发vue当中的 ...
- React学习小记--setState的同步与异步
react中,state不能直接修改,而是需要使用setState()来对state进行修改,那什么时候是同步而什么时候是异步呢? 基础代码: setCounter = (v) => { thi ...
- react中this.setState的理解
this.setState作用? 在react中要修改this.state要使用this.setState,因为this.state只是一个对象,单纯的修改state并不会触发ui更新.所以我们需要用 ...
- react中的setState的使用和深入理解
前端框架从MVC过渡到MVVM.从DOM操作到数据驱动,一直在不断的进步着,提升着, angular中用的是watcher对象,vue是观察者模式,react就是state了,他们各有各的特点,没有好 ...
- React中的setState到底发生了什么?
https://yq.aliyun.com/ziliao/301671 https://segmentfault.com/a/1190000014498196 https://blog.csdn.ne ...
- 3.React中的setstate的几个现象
转载segfault 上面的一篇文章,https://segmentfault.com/a/1190000014498196 1.在同一个方法中多次setState是会被合并的,并且对相同属性的设置只 ...
- 九、React中的组件、父子组件、React props父组件给子组件传值、子组件给父组件传值、父组件中通过refs获取子组件属性和方法
一.概述 React中的组件: 解决html 标签构建应用的不足. 使用组件的好处:把公共的功能单独抽离成一个文件作为一个组件,哪里里使用哪里引入. [父子组件]:组件的相互调用中,我们把调用者称为父 ...
- 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因
声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/det ...
- react 中的 setState
语法:setState(newState [,callback]) 1.只要有入门基础的同学都知道 setState({...}) 是更新组件中的 state 内容 2.但是,setState 是异步 ...
随机推荐
- 如何使用 Helm 在 K8s 上集成 Prometheus 和 Grafana|Part 3
在本教程的前两部分,我们分别了解和学习了Prometheus 和 Grafana 的基本概念和使用的前提条件,以及使用 Helm 在 Kubernetes 上安装 Prometheus. 在今天的教程 ...
- Makeflie脚本使用
1.目标 2.Makefile的作用 自动化编译仿真 文件有引用层级关系,Tb会引用RTL顶层,RTL顶层也会引用一些其他的小的模块,编译的时候被引用的文件需要先进行编译. 脚本有两种模式,debug ...
- Laravel路由匹配
Route常规用法如下,特别是最后一个传参之后可以进行正则匹配,非常好用. //@后面内容为所要访问的方法 Route::get('foo', 'Photos\AdminController@meth ...
- 问题--去除CSDN水印
1.问题如上 有时候需要使用其中的图片,但是水印很让人烦恼 确实可以用PS中的修复画笔工具,修复工具等进行处理 但是当水印覆盖到字体时,就会破坏到原有字体 2.解决方式 从CSDN添加水印的方式入手 ...
- PolarD&N2023秋季个人挑战赛—Misc全解
签个到叭 题目信息 压缩包带密码,放到010查看PK头错误,改回去.. 解压后得到 562+5Yiw5Lmf5LiN6IO96L+Z5LmI566A5Y2V5ZGA77yM5b+r5p2l55yL55 ...
- js - setInterval的停止与重新启动
使用js处理问题的时候,我们可能会经常使用到setInterval()来进行定时任务或者轮询的操作,那么如何让setInterval停止和重新启动呢,下边的代码就可以实现的呦,如果有更好的方法,不吝赐 ...
- [转帖]TiDB 数据库统计表的大小方法
简介:TiDB统计表的大小,列出了一些方法: 1.第一种的统计方式: 基于统计表 METRICS_SCHEMA.store_size_amplification 要预估 TiDB 中一张表的大小,你可 ...
- 【转帖】Java Full GC (Ergonomics) 的排查
文章目录 1. Full GC (Ergonomics) 1.1 Java 进程一直进行 Full GC 1.2 Full GC 的原因 1.3 检查堆占用 2. 代码检查 3. 解决方式 1. Fu ...
- Python学习之十_paramiko的简单学习
Python学习之十_paramiko的简单学习 简介 pywinrm 是python用于连接访问windows的工具 paramiko 是python用于连接访问linux的工具 ansible等工 ...
- [转帖]深入内存/主存:解剖DRAM存储器
https://zhuanlan.zhihu.com/p/561501585 2022/9/9更新:经过和评论区大佬的交流,准备研读一下JEDEC标准,主要是加深自己对banking和访存加速的理解( ...