壹 ❀ 引

了解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,那么建议在setStatecallback或者声明周期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时的statethis.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 都是异步的,比如合成事件onClickonBlur,其次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,唯一的办法就是立刻重新渲染父组件,而这种场景下,已经与我们最初的批量合并处理减少重复渲染相违背了。

    而为了解决这个问题,reactthis.propsthis.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 异步真的只是为了性能吗?

React setState 同步异步背后的故事

react官网setState实际做了什么?

setState是同步的还是异步的?

React 中setState更新state何时同步何时异步?

React 中 setState() 为什么是异步的

react中的setState是同步还是异步?react为什么要将其设计成异步?的更多相关文章

  1. React中this.setState是同步还是异步?为什么要设计成异步?

    在使用react的时候,this.setState为什么是异步呢? 一直以来没有深思这个问题.昨天就此问题搜索了一下. react创始人之一 Dan Abramovgaearon在GitHub上回答了 ...

  2. vue为什么要设计成异步队列渲染

    异步队列渲染 上一篇文章是在vue2.0 中通过Object.defineProperty去拦截并监听数据变化的响应式原理,这篇文章将会沿着图谱继续深入探索,在依赖被通知变化了之后,会触发vue当中的 ...

  3. React学习小记--setState的同步与异步

    react中,state不能直接修改,而是需要使用setState()来对state进行修改,那什么时候是同步而什么时候是异步呢? 基础代码: setCounter = (v) => { thi ...

  4. react中this.setState的理解

    this.setState作用? 在react中要修改this.state要使用this.setState,因为this.state只是一个对象,单纯的修改state并不会触发ui更新.所以我们需要用 ...

  5. react中的setState的使用和深入理解

    前端框架从MVC过渡到MVVM.从DOM操作到数据驱动,一直在不断的进步着,提升着, angular中用的是watcher对象,vue是观察者模式,react就是state了,他们各有各的特点,没有好 ...

  6. React中的setState到底发生了什么?

    https://yq.aliyun.com/ziliao/301671 https://segmentfault.com/a/1190000014498196 https://blog.csdn.ne ...

  7. 3.React中的setstate的几个现象

    转载segfault 上面的一篇文章,https://segmentfault.com/a/1190000014498196 1.在同一个方法中多次setState是会被合并的,并且对相同属性的设置只 ...

  8. 九、React中的组件、父子组件、React props父组件给子组件传值、子组件给父组件传值、父组件中通过refs获取子组件属性和方法

    一.概述 React中的组件: 解决html 标签构建应用的不足. 使用组件的好处:把公共的功能单独抽离成一个文件作为一个组件,哪里里使用哪里引入. [父子组件]:组件的相互调用中,我们把调用者称为父 ...

  9. 深刻理解Java中final的作用(一):从final的作用剖析String被设计成不可变类的深层原因

    声明:本博客为原创博客,未经同意,不得转载!小伙伴们假设是在别的地方看到的话,建议还是来csdn上看吧(原文链接为http://blog.csdn.net/bettarwang/article/det ...

  10. react 中的 setState

    语法:setState(newState [,callback]) 1.只要有入门基础的同学都知道 setState({...}) 是更新组件中的 state 内容 2.但是,setState 是异步 ...

随机推荐

  1. MAUI使用Masa blazor组件库

    上一篇(点击阅读)我们实现了UI在Web端(Blazor Server/Wasm)和客户端(Windows/macOS/Android/iOS)共享,这篇我加上 Masa Blazor组件库的引用,并 ...

  2. vscode - Prettier插件 统一代码风格规范,保存自动格式化代码

    安装 Prettier - Code formatter prettier安装完毕,使用shift+alt+f就可格式化代码. 如果需要自动保存,要在系统设置中增加"editor.forma ...

  3. 软考下午科目——第三章——UML分析与设计

    UML分析与设计 大纲要求: 学会面向对象的分析与设计,掌握UML描述方法 UML基础知识 面向对象的分析与设计 面向对象方法是一种运用对象.类.继承.封装.聚合.关联.消息.多态性等概念来构造系统的 ...

  4. k8s~istio的安装与核心组件

    安装istio 在线安装:https://istio.io/latest/docs/setup/getting-started/#download 或者直接在这里下载:https://github.c ...

  5. parquet极简学习

    parquet极简学习 摘要 parquet的概念: Parquet文件是一种列式存储文件格式,广泛应用于大数据处理框架, 如Apache Hadoop和Apache Spark. 它通过将数据组织成 ...

  6. [转帖]tidb4.0.4使用tiup扩容TiKV 节点

    https://blog.csdn.net/mchdba/article/details/108896766 环境:centos7.tidb4.0.4.tiup-v1.0.8 添加两个tikv节点   ...

  7. [转帖]MioIO读/写性能测试

    https://www.jianshu.com/p/a0a84f91b16f   image.png COSBench是Intel团队基于java开发,是一个测试云对象存储系统的分布式基准测试工具,全 ...

  8. [转帖]Intel固态硬盘总结

    https://www.cnblogs.com/hongdada/p/17326247.html 2012年推出的S3700,采用的是25nm闪存颗粒. 2015年推出s3710,采用的是20nm闪存 ...

  9. [转帖]a.out、coff、elf三种文件格式

    补充:a.out早期并不是elf格式的,而是unix下另一种可执行格式,新的a.out是 本文讨论了 UNIX/LINUX 平台下三种主要的可执行文件格式:a.out(assembler and li ...

  10. 一次OOM事故的学习过程

    事故过程 周二下午得到消息, 希望帮忙分析dump文件. 告知dump大小为42G大小. 一般机器没这么大的内存进行处理. 建议现场上传到百度云盘, 然后我这边进行下载. 时间进度为: 11.57创建 ...