setState是同步还是异步的?

2023-09-21 11:24:57

您好,如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

以下内容针对React v18以下版本。

前言

setState到底是同步还是异步?很多人可能面试都被问到过,就好比这道代码输出题:

constructor(props) {
  super(props);
  this.state = {
    data: 'data'
  }
}

componentDidMount() {
  this.setState({
    data: 'did mount state'
  })

  console.log("did mount state ", this.state.data);
  // did mount state data

  setTimeout(() => {
    this.setState({
      data: 'setTimeout'
    })

    console.log("setTimeout ", this.state.data);
  })
}

这段代码的输出结果,第一个console.log会输出data,而第二个console.log会输出setTimeout。也就是第一次setState的时候,是异步更新的,而第二次setState的时候,它又变成了同步更新,是不是有点晕呢?我们去源码里看一下setState更新调度的时候到底做了些什么。

探针

setState被调用后最终会走到scheduleUpdateOnFiber函数中,那我们看一下这个函数做了些什么呢?

if (executionContext === NoContext) {
  // Flush the synchronous work now, unless we're already working or inside
  // a batch. This is intentionally inside scheduleUpdateOnFiber instead of
  // scheduleCallbackForFiber to preserve the ability to schedule a callback
  // without immediately flushing it. We only do this for user-initiated
  // updates, to preserve historical behavior of legacy mode.
  flushSyncCallbackQueue();
}

executionContext代表了React目前所处的阶段,而NoContext你可以理解为是React没活干的状态,而flushSyncCallbackQueue里面就会去同步调用我们的this.setState,也就是说同步更新我们的state。所以,我们已经知道了,当executionContextNoContext的时候,我们的setState就是同步的。那什么地方会改变executionContext的值呢?

我们随便找几个地方看看:

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;
  // ...省略
}

function batchedUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  // ...省略
}

React进入它自己的调度步骤时,会给executionContext赋予不同的枚举,表示不同的操作和目前React所处的调度状态,而executionContext的初始值就是NoContext,所以只要你不进入React的调度流程,这个值就是NoContext,那你的setState就是同步的。

那在useState呢?自从React出了hooks之后,函数组件也能拥有自己的状态,那么如果我们调用它的第二个参数去setState更改状态,和类组件的this.setState是一样的效果吗?

没错,因为useStateset函数最终也会走到scheduleUpdateOnFiber,所以在这一点上和this.setState是没有区别的,相当于使用了一个通用函数。

但是值得注意的是,当我们调用this.setState的时候,React会自动帮我们做一个state的合并,而hook则不会,所以我们在使用的时候更着重注意这一点。

举个例子:

// 类组件中
state = {
  data: "data",
  data1: "data1",
};

this.setState({ data: "new data" });
console.log(state);
// { data: 'new data',data1: 'data1' }

// 函数组件中
const [state, setState] = useState({ data: "data", data1: "data1" });
setState({ data: "new data" });
console.log(state);
// { data: 'new data' }

但是如果你自己去尝试在函数组件中的setTimeout中去调用setState之后,打印state,你会发现并没有改变,这时你就会很疑惑,为什么呢?这不是同步执行的么?这其实是一个闭包问题,实际上拿到的还是上一个state,那打印出来的值自然也还是上一次的,此时真正的state已经改变了。

相信看到这里对于标题你已经有了答案了吧?只要你进入了React的调度流程,那就是异步的。只要你没有进入React的调度流程(executionContext === NoContext),那就是同步的。什么情况不会进入React的调度流程?setTimeout、setInterval,直接在DOM上绑定原生事件等。这些都不会走React的调度流程,你在这种情况下调用setState,那这次setState就是同步的。否则就是异步的。而setState同步执行的情况下,DOM也会被同步执行更新,也就意味着如果多次setState会导致多次更新,这也是毫无意义且浪费性能的。

setTimeout、原生事件中调用setState的操作确实比较少见,还是先看一个案例:

const fetch = async () => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('fetch data');
    }, 300);
  })
}

componentDidMount() {
  (async () => {
    const data = await this.fetch();
    this.setState({data});
    console.log("data: ", this.state);
    // data: fetch data
  })()
}

在生命周期componentDidMount挂载阶段时发送了一个网络请求,然后拿到请求响应结果后再调用setState,这时候我们用了async/await来处理。

这时候我们会发现其实setState变成同步了,为什么呢?componentDidMount不是React的内置钩子函数吗?这难道都不算React的调度环境吗?因为componentDidMount执行完毕后,就已经退出了React调度,而请求的代码是异步的,相当于队列中的宏任务还没处理完毕,等结果请求回来以后,setState才会执行。async函数中await后面的代码其实是异步执行的,这就和在setTimeout中执行setState是同样的效果,所以我们的setState就变成同步的了。

那如果变成同步的情况下滥用setState会出现什么坏处呢?我们来看看在非React调度环境下调用setState会发生什么:

this.state = {
  data: 'init data',
}

componentDidMount() {
    setTimeout(() => {
      this.setState({data: 'data 1'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
      this.setState({data: 'data 2'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
      this.setState({data: 'data 3'});
      // console.log("dom value", document.querySelector('#state').innerHTML);
    }, 1000)
}

render() {
  return (
    <div id="state">
      {this.state.data}
    </div>
  );
}

我们先看一下console的输出结果:

image.png

可以看到,console的结果是符合预期的,在setTimeout中,属于非React调度环境,在1秒后同步打印了三个最新的结果。

但是界面上出现了从最早的init data直接变成了data 3,这是为啥呢?我们每次都能在DOM上拿到最新的state,是因为React已经把state的修改同步更新了,但是为什么界面上没有显示出来?因为对于浏览器来说,渲染线程JS线程是互斥阻塞的,React代码运行调度时,浏览器是无法渲染的。所以实际上我们把DOM更新了,但是state又被修改了,React只好再做一次更新,这样反复了三次,最终React代码执行完毕后,浏览器才把最终的结果渲染到了页面上,也意味着前两次更新是无用无意义的。

我们把setTimeout去掉,就会发现三次输出都为init data,因此此时的setState就变成了异步的,会把三次更新批量合并到一次去执行,在渲染上也不会出现问题。所以当setState变成同步时就要注意,不要写出让React多次更新组件的代码,这样是毫无意义的。

结尾

React已经帮助我们做了很多优化措施,但是有时代码不同的实现方式导致了React的性能优化失败,相当于我们自己做了反优化,因此深入理解React的运行理解对于日常开发的帮助也是很大的。

如果喜欢我的文章,可以关注我的公众号「量子前端」,将不定期关注推送前端好文~

更多推荐

构建自动化测试环境:使用Docker和Selenium!

随着软件开发的日益复杂和迭代速度的加快,自动化测试被越来越广泛地应用于软件开发流程中。它能够提高测试效率、减少测试成本,并保证软件质量的稳定性。在构建自动化测试环境方面,Docker和Selenium是两个非常有用的工具。下面将介绍如何使用Docker和Selenium构建自动化测试环境。一、Docker简介Docke

电商业务--技术负责人 250K*15

职位描述研发团队管理系统搭建技术管理系统架构岗位职责负责/参与到中大型负责系统的整体架构和设计;根据业务特点和行业最佳实践,设计符合多个市场物流业务需求,且具备可扩展能力的系统架构和业务架构承担团队稳定性建设工作,包括物流全球多机房调度、稳定性治理、资损防控、容灾降级等深入理解业务,强技术驱动,能够深入挖掘业务痛点,调

2023-9

内核向应用层发送netlink单播消息:nlmsg_unicast->netlink_unicast->netlink_sendskb->__netlink_sendskb->把skb链入structsock的sk_receive_queue链表中,再调用sk->sk_data_ready(sk);->sock_def

JavaScript 的面向对象基础,设计模式中的原型模式(设计模式与开发实践 P2)

文章目录1.1动态类型语言和鸭子类型1.2多态1.3封装封装数据封装实现封装类型1.4原型模式和基于原型继承的JavaScript对象系统C#原型模式JS原型模式在学习JS设计模式之前需要了解一些设计模式基础,如果不是JavaScript用户可以直接跳到设计模式篇的讲解~1.1动态类型语言和鸭子类型编程语言按照数据类型

深度分析Oracle中的NULL

【squids.cn】全网zui低价RDS,免费的迁移工具DBMotion、数据库备份工具DBTwin、SQL开发工具等关键点特殊值NULL意味着没有数据,它声明了该值是未知的事实。默认情况下,任何类型的列和变量都可以取这个值,除非它们有一个NOTNULL约束。此外,数据库管理系统会自动向包含在表的主键中的列添加NOT

七、【漏洞复现】YApi接口管理平台远程代码执行漏洞

七、【漏洞复现】YApi接口管理平台远程代码执行漏洞7.1、漏洞原理若YApi对外开放注册功能,攻击者可在注册并登录后,通过构造特殊的请求执行任意代码,接管服务器。7.2、影响版本YApi<=V1.92All7.3、指纹识别1.有注册登陆主页2.使用指纹识别类平台识别。7.4、漏洞复现1.注册账号2.新建项目-名称随意

uniapp引入小程序原生插件

怎么在uniapp中使用微信小程序原生插件,以收钱吧支付插件为例1、在manifest.json里的mp-weixin中增加插件配置"mp-weixin":{"appid":"你的小程序appid","setting":{"urlCheck":false},"usingComponents":true,//在下面配置插

1.9python基础语法——运算符

1)算数运算符运算符描述实例+加1+1输出结果为2-减1-1输出结果为0*乘2*2输出结果为4/除10/2输出结果为5//整除9//4输出结果为2%取余9%4输出结果为1**指数2***4输出结果为16,即2*222()小括号小括号用来提高运算优先级,即(1+2)*3输出结果为9注意:混合运算优先级顺序:()高于**高

Laravel5使用box/spout扩展,大文件导出CSV文件

一、背景早期开发的系统,使用laravel框架,版本V5.4,项目经理导出3年的数据,由于数据量较大,浏览器卡死。一次性无法导出,某位程序员告知按月去导出,之后在拼凑,这。。搁谁受的了,我担心投诉,加个班优化下。二、优化方案导出数据的Sql,对应创建索引,提高查询速度查询结果集使用chunk()方法拆分较小集合使用bo

Hive 的权限管理

目录​编辑一、Hive权限简介1.1hive中的用户与组1.1.1用户1.1.2组1.1.3角色1.2使用场景1.2.1hivecli1.2.2hiveserver21.2.3hcatalogapi1.3权限模型1.3.1StorageBasedAuthorizationintheMetastoreServer1.3.

竞赛选题 基于机器视觉的火车票识别系统

文章目录0前言1课题意义课题难点:2实现方法2.1图像预处理2.2字符分割2.3字符识别部分实现代码3实现效果最后0前言🔥优质竞赛项目系列,今天要分享的是基于机器视觉的火车票识别系统该项目较为新颖,适合作为竞赛课题方向,学长非常推荐!🧿更多资料,项目分享:https://gitee.com/dancheng-sen

热文推荐