一篇文章彻底理解setState是同步还是异步!

news2025/1/24 0:47:38

本文内容均针对于18.x以下版本

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 的时候,它又变成了同步的。是不是有点晕?不慌,我们去源码中看看它到底干了什么。

结论

先把结论放到前面,懒得看的同学可以直接看结论了。

只要你进入了 react 的调度流程,那就是异步的。只要你没有进入 react 的调度流程,那就是同步的。什么东西不会进入 react 的调度流程? setTimeout setInterval ,直接在 DOM 上绑定原生事件等。这些都不会走 React 的调度流程,你在这种情况下调用 setState ,那这次 setState 就是同步的。 否则就是异步的。

而 setState 同步执行的情况下, DOM 也会被同步更新,也就意味着如果你多次 setState ,会导致多次更新,这是毫无意义并且浪费性能的。

scheduleUpdateOnFiber

setState 被调用后最终会走到 scheduleUpdateOnFiber 这个函数里面来,我们来看看这里面又做了什么:

function scheduleUpdateOnFiber(fiber, expirationTime) {
  checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);
  var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);

  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime);
  recordScheduleUpdate(); // TODO: computeExpirationForFiber also reads the priority. Pass the
  // priority as an argument to that function and this one.

  var priorityLevel = getCurrentPriorityLevel();

  if (expirationTime === Sync) {
    if ( // Check if we're inside unbatchedUpdates
    (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
    (executionContext & (RenderContext | CommitContext)) === NoContext) {
      // Register pending interactions on the root to avoid losing traced interaction data.
      schedulePendingInteractions(root, expirationTime); // This is a legacy edge case. The initial mount of a ReactDOM.render-ed
      // root inside of batchedUpdates should be synchronous, but layout updates
      // should be deferred until the end of the batch.

      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root);
      schedulePendingInteractions(root, expirationTime);

      // 重点!!!!!!
      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();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

  if ((executionContext & DiscreteEventContext) !== NoContext && ( // Only updates at user-blocking priority or greater are considered
  // discrete, even inside a discrete event.
  priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
    // This is the result of a discrete event. Track the lowest priority
    // discrete update per root so we can flush them early, if needed.
    if (rootsWithPendingDiscreteUpdates === null) {
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);

      if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
        rootsWithPendingDiscreteUpdates.set(root, expirationTime);
      }
    }
  }
}

我们着重看这段代码:

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 。所以,我们知道了,当 executionContext 为 NoContext 的时候,我们的 setState 就是同步的。那什么地方会改变 executionContext 的值呢?

我们随便找几个地方看看

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

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

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

useState的setState

自从 raect 出了 hooks 之后,函数组件也能有自己的状态,那么如果我们调用它的 setState 也是和 this.setState 一样的效果吗?

对,因为 useState 的 set 函数最终也会走到 scheduleUpdateOnFiber ,所以在这一点上和 this.setState 是没有区别的。

但是值得注意的是,我们调用 this.setState 的时候,它会自动帮我们做一个 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' }

但是如果你自己去尝试在 function 组件的 setTimeout 中去调用 setState 之后,打印 state ,你会发现他并没有改变,这时你就会很疑惑,为什么呢?这不是同步执行的吗?

这是因为一个闭包问题,你拿到的还是上一个 state ,那打印出来的值自然是上一次的,此时真正的 state 已经被改变了。那有没有其他的方法可以观察到 function 函数的同步行为?有,我们下面再介绍。

案例分析

setTimeout 、原生事件内调用 setState 的操作确实比较少见,但是下面这种写法一定很常见了。

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
    })()
  }

我们在 didMount 的时候发了一个请求,然后再将结果 setState ,这时候我们用了 async/await 来进行处理。

这时候我们会发现其实 setState 也会变成同步了,为什么呢?因为componentDidMount执行完毕后,就已经退出了 react 的调度,而我们请求的代码还没有执行完毕,等结果请求回来以后 setState 才会执行。async 函数中 await 后面的代码其实是异步执行的。这和我们在 setTimeout 中执行 setState 其实是一个效果,所以我们的 setState 就变成同步的了。

如果它变成同步会有什么坏处呢?我们来看看如果我们多次调用了 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>
  );
}

这是在浏览器运行的结果

这样来看的话,其实也并没有什么,每次刷新后最终还是会显示 data 3 ,但是我们将代码中 console.log 的注释去掉,再看看:

我们每次都能在 DOM 上拿到最新的 state ,这是因为 react 已经把 state 的修改同步更新了,但是为什么界面没有显示出来?因为对浏览器来说,渲染线程 和 js线程 是互斥的, react 代码运行时浏览器是没办法渲染的。所以实际上我们已经把 DOM 更新了,但是 state 又被修改了, react 只好再做一次更新,这样反复了三次,最后 react 代码执行完毕后,浏览器才把最终的结果渲染到了界面上。这也就意味着其实我们已经做了两次无用的更新。

我们把 setTimeout 去掉,就会发现三次都输出了 init data ,因为此时的 setState 是异步的,会把三次更新合并到一次去执行。

所以当 setState 变成同步的时候就要注意,不要写出让 react 多次更新组件的代码,这是毫无意义的。

而这里也回答了之前提出的问题,如果我们想在 function 函数中观察到同步流程,大家可以去试试当你在 setTimeout 中 setState 之后, DOM 里面的内容会不会改变。

结语

react 已经帮助我们做了很多优化措施,但是有时候也会因为代码不同的实现方式而导致 react 的性能优化失效,相当于我们自己做了反优化。所以理解 react 的运行原理对我们日常开发确实是很有帮助的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/396997.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Sentinel架构篇 - 来源访问控制

来源访问控制&#xff08;黑白名单&#xff09; 概念 Sentinel 提供了黑白名单限制资源能否通过的功能。如果配置了白名单&#xff0c;则只有位于白名单的请求来源的对应的请求才能通过&#xff1b;如果配置了黑名单&#xff0c;则位于黑名单的请求来源对应的请求不能通过。 …

图形报表ECharts

图形报表ECharts1 图形报表ECharts1.1 ECharts简介-富客户端图表库ECharts缩写来自Enterprise Charts&#xff0c;商业级数据图表&#xff0c;是百度的一个开源的使用JavaScript实现的数据可视化工具&#xff0c;可以流畅的运行在PC和移动设备上&#xff0c;兼容当前绝大部分浏…

【3.8】操作系统内存管理、Redis数据结构、哈希表

内存满了&#xff0c;会发生什么&#xff1f; 当应用程序读写了这块虚拟内存&#xff0c;CPU 就会去访问这个虚拟内存&#xff0c; 这时会发现这个虚拟内存没有映射到物理内存&#xff0c; CPU 就会产生缺页中断&#xff0c;进程会从用户态切换到内核态&#xff0c;并将缺页中…

MySQL索引15连问,抗住!

1. 索引是什么&#xff1f;索引是一种能提高数据库查询效率的数据结构。它可以比作一本字典的目录&#xff0c;可以帮你快速找到对应的记录。索引一般存储在磁盘的文件中&#xff0c;它是占用物理空间的。正所谓水能载舟&#xff0c;也能覆舟。适当的索引能提高查询效率&#x…

实战小项目之视频监控(1-2)

实战小项目之视频监控&#xff08;1-2&#xff09; Nginx 移植 前面也给大家提到了&#xff0c;我们可以使用 Nginx 来搭建 RTMP 流媒体服务器&#xff0c;譬如你可以在一台公网 IP 主 机上搭建流媒体服务器&#xff0c;当然&#xff0c;笔者并没有这个条件&#xff1b;这里我…

2023年计算语言学和自然语言处理国际会议(CLNLP 2023)

2023年计算语言学和自然语言处理国际会议&#xff08;CLNLP 2023&#xff09; 重要信息 会议网址&#xff1a;www.clnlp.org 会议时间&#xff1a;2023年8月18-20日 召开地点&#xff1a;中国南京 截稿时间&#xff1a;2023年6月31日 录用通知&#xff1a;投稿后2周内 收…

MATLAB绘制三Y轴坐标图:补充坐标轴及字体设置

三轴坐标图 1 函数 MATLAB绘制三轴图函数可见MATLAB帮助-multiplotyyy 基础图形绘制是很简单&#xff0c;但坐标轴及字体设置该如何实现呢&#xff1f; 本文以以下几个例子为例&#xff0c;希望可以解决在利用MATLAB绘制三轴坐标图时常见的疑惑。 2 案例 2.1 案例1&#xf…

大数据框架之Hive:第6章 查询

第6章 查询 6.1 基础语法 1&#xff09;官网地址 https://cwiki.apache.org/confluence/display/Hive/LanguageManualSelect 2&#xff09;查询语句语法&#xff1a; SELECT [ALL | DISTINCT] select_expr, select_expr, ...FROM table_reference -- 从什么表查[WHE…

Java的概述和运行方式

目录 一.Java是什么&#xff1f; 1.1Java的目前状况和学习需求 1.2Java的平台分类和特点 二.Java程序的运行方式 2.1 Java的程序结构 2.2 JDK、JRE、JVM的关系 2.3 Java运行详情 总结 &#x1f63d;个人主页&#xff1a;tq02的博客_CSDN博客-领域博主 &#x1f308;理想…

R语言ggplot2 | 用百分比格式表示数值

&#x1f4cb;文章目录Percent() 函数介绍例子1&#xff0c;在向量中格式化百分比&#xff1a;例子2&#xff0c;格式化数据框列中的百分比&#xff1a;例子3&#xff0c;格式化多个数据框列中的百分比&#xff1a;如何使用percent()函数在绘图过程展示通常在绘图时&#xff0c…

Linux 学习笔记

一、 概述 1. 操作系统 ① 计算机由硬件和软件组成 ② 操作系统属于软件范畴&#xff0c;主要作用是协助用户调度硬件工作&#xff0c;充当用户和计算机硬件之间的桥梁 ③ 常见的操作系统 &#x1f920; PC端&#xff1a;Windows、Linux、MacOS&#x1f920; 移动端&#…

Android中级——屏幕和绘图

屏幕和绘图屏幕系统屏幕密度独立像素密度dp单位转换XML绘图&#xff08;需放在Drawable&#xff09;BitmapShapeLayerSelector绘图技巧CanvasLayerPorterDuffXfermodeShaderPathEffectSurfaceView屏幕 屏幕大小&#xff1a;指屏幕对角线长度&#xff0c;单位为寸分辨率&#x…

管理类书籍推荐

管理类书籍对于每一位想要获得管理能力提升或者实现职业生涯更上一层楼的企业管理者或领导者而言&#xff0c;都是不可或缺的一项重要学习工具。作为管理工作从事者的职场必需品&#xff0c;一本出色的管理类书籍可以为我们提供大量宝贵的经验与专业建议&#xff0c;从而让管理…

ChatGPT助力校招----面试问题分享(二)

1 ChatGPT每日一题&#xff1a;DC-DC与LDO的区别 问题&#xff1a;介绍一下DC-DC与LDO的区别 ChatGPT&#xff1a;DC-DC和LDO都是电源管理电路&#xff0c;它们的主要作用是将输入电压转换为所需的输出电压&#xff0c;以供电子设备使用。但是&#xff0c;它们之间存在一些重…

GlassFish的安装与使用

一、产品下载与安装glassfish下载地址&#xff1a;https://download.oracle.com/glassfish/5.0.1/release/index.html下载后解压即完成安装&#xff0c;主要目录说明&#xff1a;bin目录&#xff1a;为asadmin命令所在目录。glassfish为主目录&#xff1a;glassfish\bin目录为命…

C++基础了解-13-C++ 数组

C 数组 一、C 数组 C 支持数组数据结构&#xff0c;它可以存储一个固定大小的相同类型元素的顺序集合。数组是用来存储一系列数据&#xff0c;但它往往被认为是一系列相同类型的变量。 数组的声明并不是声明一个个单独的变量&#xff0c;比如 number0、number1、…、number9…

Android特别的数据结构(一) SparseArray源码解析

1.数据结构 class SparseArray<E> implements Cloneable 由两个数组构成&#xff0c;一个数组mKeys类型为int[]&#xff0c;存放Key&#xff0c;一个数组mValues类型为 E[]&#xff0c;存放Value。Key数组升序排列。默认初始容量&#xff1a;10扩容&#xff1a; 如果当…

Hbuilder 下载与安装教程

文章目录Hbuilder下载与安装教程Hbuilder简介一&#xff0c;下载Hbuilder二&#xff0c;安装Hbuilder三&#xff0c;简单使用四&#xff0c;Hbuilderx 调试Hbuilder下载与安装教程 Hbuilder简介 Builder是DCloud&#xff08;数字天堂&#xff09;推出的一款支持HTML5的Web开发…

你还在手撸SQL?ChatGPT笑晕在厕所

文章目录你还在手撸SQL&#xff1f;ChatGPT笑晕在厕所一、背景二、面向Chat编程1. 数据库设计2. 建表语句3. 加中文注释4. 数据模拟5. 查询成绩6. 修改课程任课老师7. 删除课程8. 删除一个有关联数据的课程总结你还在手撸SQL&#xff1f;ChatGPT笑晕在厕所 一、背景 经典3表设…

【项目精选】基于SSH的医院在线挂号系统(视频+论文+源码)

点击下载源码 医院挂号系统主要用于实现医院的挂号&#xff0c;前台基本功能包括&#xff1a;用户注册、用户登录、医院查询、挂号、取消挂号、修改个人信息、退出等。 后台基本功能包括&#xff1a;系统管理员登录、医院管理、科室管理、公告管理、退出系统等。 本系统结构如…