当前位置: 首页 > 知识库问答 >
问题:

javascript - `setTimeout` 宏任务下改变了 `setState` 执行顺序,请问这是什么原因?

双元魁
2024-08-04

演示:https://stackblitz.com/edit/vitejs-vite-nfngmd?file=src%2Fcom...

点最底部按钮:1 和 2,文件名 /src/com/TestCom.tsx

先看一段简单的代码:

    console.log(1);
    setTimeout(() => console.log(2), 0);
    new Promise((resolve) => {
      console.log(3);
      resolve(4);
    }).then((num) => console.log(num));
    setData(() => {
      console.log(5);
      return { a: Date.now() };
    });
    console.log(6);

这段顺序执行如下:

  • 1、3、6,是上下文
  • 4、5,是微任务
  • 2 是宏任务

也就是:1、3、6、4、5、2


现在代码不变将其全部包裹在 setTimeout 这个宏任务里

setTimeout(() => {
    // 上面的代码
}, 0);

这个时候发现 setState 是最后执行,即:1、3、6、4、2、5


在上面演示中 TestCom.tsx 中有两个方法:clickHandleclickTimeooutHandle

  • 最终都是执行 clickHandle
  • 不同的是 clickTimeooutHandleclickHandle 包裹在 setTimeout 中执行

这里会有 2 个不同点

第一个不同:只点按钮 1,初始化和之后每次点击不同

  • 初始化输出:1、3、5、6、4、2
  • 之后每次点:1、3、6、4、5、2

第二个不同:按钮 1 和按钮 2 不同

  • 按钮 1:1、3、6、4、5、2
  • 按钮 2:1、3、6、4、2、5

请问这是怎么回事呢?

共有3个答案

卫泉
2024-08-04

在示例中,我按1 打印的情况是"1365542",2的打印情况是"1364552"和你描述不同。
image.png
额外说一个,clickTimeooutHandleclickHandle 这两个其实不需要usecallback ,因为你的依赖项并没有变。

对问题1的猜测:你电脑cpu的问题。
对问题2的猜测:就你的描述而言,在按2的时候,2会提到5前面,是2套了两层宏任务,5套了一层宏任务一层微任务。2的两个宏任务都是0ms,都是相对于你点击的时间点而言的。到了下一帧的时候,2变成上下文,而5还有一层useState微任务。

调整顺序可以检查是否是平级的内容,在按钮1中,4和5是平级的,在按钮2中,4和5不是同一种任务。按我下图的顺序,两个按钮打印的顺序都是1364-552
image.png
image.png

景靖琪
2024-08-04

第一次点击和之后点击不同是因为 react 更新策略的原因,可以参考:

关于React FC useState,如何解释以下console.log?

按钮 1 和按钮 2 输出不同是因为 setState 执行环境不同,直接在事件处理回调函数中执行和在 setTimeout 回调函数中执行会有不同的行为,参考:

一文搞懂React 的 setState 机制

谭勇
2024-08-04

在 JavaScript 中,事件循环(Event Loop)和任务队列(Task Queues)的工作方式导致了这种看似复杂的执行顺序。理解这一点对于解决你的问题至关重要。

基础知识

  • 宏任务(MacroTasks):包括整体代码执行、setTimeoutsetIntervalsetImmediate(Node.js)、I/O 操作等。
  • 微任务(MicroTasks):包括 Promise.then().catch().finally()MutationObserver(DOM 变更的观察者)、process.nextTick(Node.js)等。

分析

  1. 直接执行代码(非 setTimeout 包裹)

    • 当代码直接执行时,首先输出 1、3、6(同步代码)。
    • 接着,Promise 的 .then() 被推入微任务队列。
    • setData(假设这是 React 的 setState 或类似的异步更新状态函数)被调用,但状态更新通常不会立即反映在 DOM 或日志中,它会被 React 的调度器(Scheduler)安排在未来的某个时间点。
    • 然后,遇到 setTimeout,它会被推入宏任务队列,但此时不执行。
    • 当前执行栈清空后,检查微任务队列并执行(输出 4、5,其中 5 是由 setData 引起的,但可能在微任务队列中作为 React 调度的一部分执行)。
    • 最后,执行宏任务队列中的 setTimeout(输出 2)。
  2. 代码被 setTimeout 包裹

    • 外部 setTimeout 将整个代码块作为一个宏任务推入队列。
    • 当这个宏任务执行时,内部代码按照同步代码、微任务、再是下一个宏任务的顺序执行。
    • 因此,setData 的调用被安排在微任务中,但在外部的 setTimeout 宏任务内。
    • 这意味着 setData 引起的状态更新(及其可能的副作用,如组件重新渲染)会在内部的微任务队列中处理,但这仍然是在外部 setTimeout 宏任务完成之前。
    • 最后,外部的 setTimeout 宏任务结束后,如果还有其他宏任务(如按钮 2 可能触发的另一个 setTimeout),它们会按顺序执行。

特定情况分析

  • 按钮 1 和 2 的差异

    • 按钮 1 直接调用 clickHandle,遵循上述分析。
    • 按钮 2 将 clickHandle 放入 setTimeout,这意味着每次点击按钮 2 都会将 clickHandle 的执行作为一个新的宏任务放入队列。因此,如果先点击按钮 1 后点击按钮 2,按钮 1 的宏任务(及其微任务)会先执行完毕,然后是按钮 2 的宏任务及其微任务。
  • 初始化与后续点击的差异

    • 初始化时可能由于组件的挂载和初始化过程,React 的状态更新和渲染行为有所不同,这可能导致 setData(或 setState)的执行和效果看起来不同。
    • 一旦组件稳定,后续的状态更新将遵循相同的规则。

结论

这种执行顺序的差异主要是由于 JavaScript 的事件循环和任务队列机制,以及 React(或类似框架)对状态更新的处理方式造成的。理解这些基本概念对于编写可靠和可预测的异步代码至关重要。

 类似资料:
  • 本文向大家介绍现在有一个宏任务,又有一个微任务两者同一层级,在微任务里面又有一个宏任务和一个微任务,请问执行顺序是什么,为什么?相关面试题,主要包含被问及现在有一个宏任务,又有一个微任务两者同一层级,在微任务里面又有一个宏任务和一个微任务,请问执行顺序是什么,为什么?时的应答技巧和注意事项,需要的朋友参考一下 宏任务——》微任务中的宏任务——》微任务中的微任务——》微任务 宏任务执行完成会去检测微

  • 有没有大佬提供下这种echart 图表的示例参考一下

  • 向ExecutorService对象提交任务的执行顺序是什么? 场景:让我们暂时假设Executor线程池大小为5,我已经向它提交了20个可运行任务,我们知道一次只能执行5个任务,其余的任务将在bucket中等待。所以我的问题是提交的任务以什么顺序执行。它是遵循FIFO数据结构还是从bucket中随机选取任务。 还有,有没有办法指定它应该以什么顺序执行。 例子:

  • 本文向大家介绍setTimeout和Promise的执行顺序?相关面试题,主要包含被问及setTimeout和Promise的执行顺序?时的应答技巧和注意事项,需要的朋友参考一下 参考回答: 首先我们来看这样一道题: 输出答案为2 10 3 5 4 1 要先弄清楚settimeout(fun,0)何时执行,promise何时执行,then何时执行 settimeout这种异步操作的回调,只有主线程

  • 目前生产出现了,handleDealData()返回的结果是[],并非是forEach执行后的数组,此问题偶发。 但是查了资料,解释说forEach的循环是同步任务。 chrome测试正常,返回非[] forEach的执行顺序是否跟机型、浏览器有关?

  • 在写后端登录请求的发现 post请求没有执行,再入口主文件也只是简单调用