代码编织梦想

本文内容均针对于18.x以下版本(18请看这篇文章=>React18 setState是同步还是异步?

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

这是在浏览器运行的结果

9a471a208912cf13300a58d09ee262ed.jpeg

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

cee8e8f599df44c73cf8f6fc7dd6aef3.jpeg

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

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

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

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

结语

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

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/GMLGDJ/article/details/129403387

理解setstate(),异步还是同步?-爱代码爱编程

state state的存在是为了动态改变组件,比如根据不同的用户操作和网络请求,来重新渲染组件。 setState()是React给我们的一个API,用来改变或定义state。 setState()的批量操作(batching) 在一个事件handler函数中,不管setState()被调用多少次,他们也会在函数执行结束以后,被归结

react 篇之浅谈 setstate is异步or同步-爱代码爱编程

React 篇之浅谈 setState is异步OR同步   react 篇主要是记录笔者之前在使用 React 进行开发时遇到的问题和坑, 趁还没有毕业 一 一 查阅文档资料总结归纳, 和大家一起分享, 以防重蹈覆辙.

react中setState()是异步的还是同步的,如何控制?-爱代码爱编程

上上篇博客我讲了setState() 的批处理合并,而setState()是异步的还是同步的,和setState() 的批处理有很大的关系,推荐先看完上上篇博客再来看这篇,会清晰很多 地址如下:setState()批处理,合并策略,控制批处理----batchUpdates setState()是异步的还是同步的? 先看异步的情况: import

React中的setState的同步异步与合并-爱代码爱编程

文章目录 前言引入同步和异步合并浅析原理图总结最后再看一道常见面试题 前言 这篇文章主要是因为自己在学习React中setState的时候,产生了一些疑惑,所以进行了一定量的收集资料和学习,并在此记录下来 引入 使用过React的应该都知道,在React中,一个组件中要读取当前状态需要访问this.state,但是更新状态却需要使用thi

setState 到底是同步的,还是异步的-爱代码爱编程

点击上方关注 前端技术江湖,一起学习,天天进步 从一道面试题说起 这是一道变体繁多的面试题,在 BAT 等一线大厂的面试中考察频率非常高。首先题目会给出一个这样的 App 组件,在它的内部会有如下代码所示的几个不同的 setState 操作: import React from "react"; import "./styles.css"; e

从react原理角度理解setState究竟是同步还是异步以及react hooks中的更新机制-爱代码爱编程

文章目录 简单记忆法原理setState主流程普通更新setTimeout中更新原生事件在react hooks中的更新机制有这样一个经典的例子react hooks中的setTimeout会同步更新吗对比一下class组件中的setTimeout再对比一下setCount中使用箭头函数的情况原因那在react hooks中想要立刻拿到新值怎么办?

React 中 setState 是一个宏任务还是微任务?-爱代码爱编程

点击上方 程序员成长指北,关注公众号 回复1,加入高级Node交流群 最近有个朋友面试,面试官问了个奇葩的问题,也就是我写在标题上的这个问题。 能问出这个问题,面试官应该对 React 不是很了解,也是可能是看到面试者简历里面有写过自己熟悉 React,面试官想通过这个问题来判断面试者是不是真的熟悉 React ????。 面试官的问法是否

react中setState()是异步的还是同步的那?是否可以控制它的同步还是异步执行?-爱代码爱编程

上一篇我们说到了setState的合并策略,而setState是同步还是异步的,和setState()的批量处理有很大的关系。 可以先看看这个文章在来看同步还是异步的!react中setState()的执行策略是什么?如何合并的那?如何控制合并? setState()是同步还是异步那? 看看一下代码的执行情况: import React, { Com

react中的setstate是同步还是异步?react为什么要将其设计成异步?_行星飞行的博客-爱代码爱编程

壹 ❀ 引 了解react的同学都知道,react遵守渲染公式UI=Render(state),状态决定了组件UI最终渲染的样子(props也可以理解为外部传入的状态),由此可见state对于react的重要性。而在实际使用中,若我们想修改状态必须得借用APIsetState,也只有通过此方法修改状态才能顺利触发react下次render,那么对于一

彻底理解同步,异步,阻塞,非阻塞_junius9的博客-爱代码爱编程

同步、异步调用 同步:线程调用后自己等待结果异步:线程调用后等待其他线程发送结果 在线程调用层面同步异步和阻塞非阻塞并没有明显的差别,可以将二者看作相同的概念。但在IO层面来看二者是有区别的。在了解他们的区别之前首先要看

问:react的setstate为什么是异步的?_beifeng11996的博客-爱代码爱编程

前言 不知道大家有没有过这个疑问,React 中 setState() 为什么是异步的?我一度认为 setState() 是同步的,知道它是异步的之后很是困惑,甚至期待 React 能出一个 setStateSync()

【go】k8s 管理系统项目33[前端部分–登录和登出]-爱代码爱编程

K8s 管理系统项目[前端部分–登录和登出] 1. 登录登出流程 1.1 登录流程 登入流程总的分为5步: 账号密码验证token生成token验证验证成功进行跳转验证失败返回/login 1.2 登出流程

[vue]提供一种网站底部备案号样式代码-爱代码爱编程

演示 vue组件型(可直接用) 组件代码:copyright-icp.vue <template> <div class="icp">{{`© ${year} ${auth

2023实习面试公司【二】-爱代码爱编程

2023实习面试第二家公司 文章目录 2023实习面试第二家公司前言一、面试官所问的问题?二、总结1.公司待遇2.推荐指数3.自己的感受 前言 某岸科技,这家公司是我从拉钩上找的第二家面试公司,

使用websocket、sockjs、stomp实现消息实时通讯功能_sockjs stomp-爱代码爱编程

客户端 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN"> <html> <head> <titl

react学习笔记-爱代码爱编程

react旧版本路由 旧版本的路由是按照组件的方式来写的 编写router/index.tsx文件 import App from "../App" import Home from "../views/Home" import About from "../views/About" import { BrowserRouter,Routes,Rou

前端布局小案例,如何创建漂亮的毛玻璃输入表单卡片效果-爱代码爱编程

在当今互联网时代,用户体验是至关重要的。当我们在设计网站或应用程序时,一个漂亮、吸引人的界面往往是吸引用户并提高用户满意度的关键因素之一。而一个好看的表单则可以提高用户提交的意愿和效率。本文将介绍如何使用HTML和CSS创建一个漂亮的毛玻璃输入表单卡片效果,以提高用户的体验和满意度。让我们开始吧! HTML部分 首先,我们需要决定

浅析 splitchunksplugin 及代码分割的意义-爱代码爱编程

本文作者为 360 奇舞团前端开发工程师 起因 有同事分享webpack的代码分割,其中提到了SplitChunksPlugin,对于文档上的描述大家有着不一样的理解,所以打算探究一下。 Q:什么是 SplitChunksPlugin?SplitChunksPlugin 是用来干嘛的? A: 最初,chunks(以及内部