壹 ❀ 引

了解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. 如何使用 Helm 在 K8s 上集成 Prometheus 和 Grafana|Part 3

    在本教程的前两部分,我们分别了解和学习了Prometheus 和 Grafana 的基本概念和使用的前提条件,以及使用 Helm 在 Kubernetes 上安装 Prometheus. 在今天的教程 ...

  2. Makeflie脚本使用

    1.目标 2.Makefile的作用 自动化编译仿真 文件有引用层级关系,Tb会引用RTL顶层,RTL顶层也会引用一些其他的小的模块,编译的时候被引用的文件需要先进行编译. 脚本有两种模式,debug ...

  3. Laravel路由匹配

    Route常规用法如下,特别是最后一个传参之后可以进行正则匹配,非常好用. //@后面内容为所要访问的方法 Route::get('foo', 'Photos\AdminController@meth ...

  4. 问题--去除CSDN水印

    1.问题如上 有时候需要使用其中的图片,但是水印很让人烦恼 确实可以用PS中的修复画笔工具,修复工具等进行处理 但是当水印覆盖到字体时,就会破坏到原有字体 2.解决方式 从CSDN添加水印的方式入手 ...

  5. PolarD&N2023秋季个人挑战赛—Misc全解

    签个到叭 题目信息 压缩包带密码,放到010查看PK头错误,改回去.. 解压后得到 562+5Yiw5Lmf5LiN6IO96L+Z5LmI566A5Y2V5ZGA77yM5b+r5p2l55yL55 ...

  6. js - setInterval的停止与重新启动

    使用js处理问题的时候,我们可能会经常使用到setInterval()来进行定时任务或者轮询的操作,那么如何让setInterval停止和重新启动呢,下边的代码就可以实现的呦,如果有更好的方法,不吝赐 ...

  7. [转帖]TiDB 数据库统计表的大小方法

    简介:TiDB统计表的大小,列出了一些方法: 1.第一种的统计方式: 基于统计表 METRICS_SCHEMA.store_size_amplification 要预估 TiDB 中一张表的大小,你可 ...

  8. 【转帖】Java Full GC (Ergonomics) 的排查

    文章目录 1. Full GC (Ergonomics) 1.1 Java 进程一直进行 Full GC 1.2 Full GC 的原因 1.3 检查堆占用 2. 代码检查 3. 解决方式 1. Fu ...

  9. Python学习之十_paramiko的简单学习

    Python学习之十_paramiko的简单学习 简介 pywinrm 是python用于连接访问windows的工具 paramiko 是python用于连接访问linux的工具 ansible等工 ...

  10. [转帖]深入内存/主存:解剖DRAM存储器

    https://zhuanlan.zhihu.com/p/561501585 2022/9/9更新:经过和评论区大佬的交流,准备研读一下JEDEC标准,主要是加深自己对banking和访存加速的理解( ...