事件循环上下文中微任务和宏任务之间的区别

IT技术 javascript node.js promise event-loop
2021-01-31 14:58:10

我刚刚阅读了 Promises/A+ 规范,偶然发现了术语 microtask 和 macrotask:请参阅http://promisesaplus.com/#notes

我以前从未听说过这些术语,现在我很好奇它们之间的区别是什么?

我已经尝试在网上找到一些信息,但我找到的只是 w3.org 档案中的这篇文章(它没有向我解释区别):http : //lists.w3.org/Archives /Public/public-nextweb/2013Jul/0018.html

此外,我发现了一个名为“macrotask”的 npm module:https : //www.npmjs.org/package/macrotask 同样,它没有阐明究竟有什么区别。

我所知道的是,它与事件循环有关,如https://html.spec.whatwg.org/multipage/webappapis.html#task-queuehttps://html.spec.whatwg 中所述.org/multipage/webappapis.html#perform-a-microtask-checkpoint

我知道理论上我应该能够根据这个 WHATWG 规范自己提取差异。但我相信其他人也可以从专家给出的简短解释中受益。

4个回答

事件循环的一次循环将正好宏任务队列中处理一个任务(该队列WHATWG 规范中简称为任务队列)。在这个宏任务完成后,所有可用的任务都将被处理,即在同一个复飞周期内。在处理这些微任务的同时,它们还可以排队更多的微任务,这些微任务都会一一运行,直到微任务队列耗尽。

这样做的实际后果是什么?

如果一个微任务递归地将其他微任务排队,则可能需要很长时间才能处理下一个宏任务。这意味着,您最终可能会遇到一个阻塞的 UI,或者您的应用程序中一些已完成的 I/O 空闲。

然而,至少有关的Node.js的process.nextTick函数(队列microtasks),存在通过process.maxTickDepth的手段对这种阻挡一个内置的保护。此值设置为默认值 1000,在达到此限制后减少对任务的进一步处理,从而允许处理下一个宏任务

那么什么时候使用呢?

基本上,当您需要以同步方式异步执行某些操作时(即,当您说在最近的将来执行此(微)任务时),请使用微任务否则,坚持使用macrotasks

例子

宏任务: setTimeoutsetIntervalsetImmediaterequestAnimationFrameI/O、UI 渲染
微任务: process.nextTickPromisesqueueMicrotaskMutationObserver

尽管事件循环中有一个微任务检查点,但这并不是大多数开发人员会遇到微任务的地方。当 JS 堆栈清空时,就会处理微任务。这可能在一个任务中多次发生,甚至在事件循环的渲染步骤中发生。
2021-03-16 14:58:10
Jake Archibald 的这篇文章帮助我理解了其中的区别:jakearchibald.com/2015/tasks-microtasks-queues-and-schedules
2021-03-18 14:58:10
process.maxTickDepth很久以前就被删除了:github.com/nodejs/node/blob/...
2021-03-20 14:58:10
您还可以使用queueMicrotask()方法添加新的微任务
2021-03-23 14:58:10
requestAnimationFrame(rAF) 不仅生成微任务。一般来说,rAF 调用会创建一个单独的队列
2021-03-28 14:58:10

规范中的基本概念

  • 一个事件循环有一个或多个任务队列。(任务队列是宏任务队列)
  • 每个事件循环都有一个微任务队列。
  • 任务队列 = 宏任务队列!= 微任务队列
  • 一个任务可能被推入宏任务队列,或微任务队列
  • 当一个任务被推入队列(微/宏)时,我们的意思是准备工作已经完成,所以现在可以执行任务了。

而事件循环流程模型如下:

调用堆栈为空时,执行以下步骤-

  1. 选择任务队列中最早的任务(任务 A)
  2. 如果任务 A 为空(表示任务队列为空),则跳转到步骤 6
  3. 将“当前正在运行的任务”设置为“任务 A”
  4. 运行“任务A”(表示运行回调函数)
  5. 将“当前正在运行的任务”设置为空,删除“任务 A”
  6. 执行微任务队列
    • (a).选择微任务队列中最早的任务(任务x)
    • (b).如果任务 x 为空(意味着微任务队列为空),跳到步骤(g)
    • (c). 将“当前正在运行的任务”设置为“任务 x”
    • (d). 运行“任务 x”
    • (e).将“当前正在运行的任务”设置为空,删除“任务x”
    • (f).选择微任务队列中下一个最早的任务,跳转到步骤(b)
    • (g). 完成微任务队列;
  7. 跳到第 1 步。

一个简化的过程模型如下:

  1. 运行宏任务队列中最旧的任务,然后将其删除。
  2. 运行微任务队列中的所有可用任务,然后删除它们。
  3. 下一轮:运行宏任务队列中的下一个任务(跳转步骤2)

要记住的事情:

  1. 当一个任务(在宏任务队列中)运行时,可能会注册新事件。因此可能会创建新任务。以下是两个新创建的任务:
    • promiseA.then() 的回调是一个任务
      • promiseA 被解决/拒绝:任务将在当前轮事件循环中被推入微任务队列。
      • promiseA 未决:该任务将在未来一轮的事件循环中被推入微任务队列(可能是下一轮)
    • setTimeout(callback,n)的回调是一个任务,会被推入macrotask队列,即使n为0;
  2. 微任务队列中的任务将在本轮运行,而宏任务队列中的任务必须等待下一轮事件循环。
  3. 我们都知道"click","scroll","ajax","setTimeout"...的回调是任务,但是我们也应该记住脚本标签中的js代码整体也是一个任务(宏任务)。
我认为它们适合微任务(例如requestAnimationFrame
2021-03-13 14:58:10
浏览器paint任务呢?他们属于哪个类别?
2021-03-24 14:58:10
这是一个很好的解释!感谢分享!。还要提到的一件事是在NodeJs 中setImmediate()是宏/任务,process.nextTick()是微/作业。
2021-03-25 14:58:10
我不知道我是否弄错了,但我有点不同意这个答案,微任务在宏任务之前运行。codepen.io/walox/pen/yLYjNRq ?
2021-03-28 14:58:10
@walox 当前脚本执行也是一个宏任务。一旦所有同步代码完成,事件循环将优先考虑微任务而不是宏任务。与您的示例一样,脚本执行后,超时回调位于宏任务/回调队列中,而Promise回调位于微任务队列中。由于一个宏任务已经完成(主脚本执行),事件循环会将Promise任务优先于超时一。因此结果。
2021-04-02 14:58:10

我认为我们不能脱离堆栈来讨论事件循环,所以:

JS 有三个“栈”:

  • 所有同步调用的标准堆栈(一个函数调用另一个函数等)
  • 用于所有具有更高优先级的异步操作的微任务队列(或作业队列或微任务堆栈)(process.nextTick、Promises、Object.observe、MutationObserver)
  • 宏任务队列(或事件队列、任务队列、宏任务队列)用于所有具有较低优先级的异步操作(setTimeout、setInterval、setImmediate、requestAnimationFrame、I/O、UI 渲染)
|=======|
| macro |
| [...] |
|       |
|=======|
| micro |
| [...] |
|       |
|=======|
| stack |
| [...] |
|       |
|=======|

事件循环是这样工作的:

  • 从堆栈中从下到上执行所有内容,并且仅当堆栈为空时,检查上面队列中发生的事情
  • 检查微堆栈并在堆栈的帮助下执行那里的所有内容(如果需要),一个接一个微任务,直到微任务队列为空或不需要任何执行,然后才检查宏堆栈
  • 检查宏堆栈并在堆栈的帮助下执行那里的所有内容(如果需要)

如果堆栈不为空,则不会触及微堆栈。如果微堆栈不为空或不需要任何执行,则不会触及宏堆栈。

总结一下:微任务队列与宏任务队列几乎相同,但那些任务(process.nextTick、Promises、Object.observe、MutationObserver)比宏任务具有更高的优先级。

微观就像宏观,但具有更高的优先级。

在这里,您拥有理解一切的“终极”代码。


console.log('stack [1]');
setTimeout(() => console.log("macro [2]"), 0);
setTimeout(() => console.log("macro [3]"), 1);

const p = Promise.resolve();
for(let i = 0; i < 3; i++) p.then(() => {
    setTimeout(() => {
        console.log('stack [4]')
        setTimeout(() => console.log("macro [5]"), 0);
        p.then(() => console.log('micro [6]'));
    }, 0);
    console.log("stack [7]");
});

console.log("macro [8]");
感谢有关 Node.js 的旁注。
2021-03-21 14:58:10
将队列称为堆栈是完全令人困惑的。
2021-03-26 14:58:10

JavaScript是高级单线程语言,解释型语言。这意味着它需要一个将 JS 代码转换为机器代码的解释器。解释器的意思是引擎。用于 chrome 的 V8 引擎和用于 safari 的 webkit。每个引擎都包含内存、调用堆栈、事件循环、计时器、Web API、事件等。

事件循环:微任务和宏任务

事件循环的概念非常简单。有一个无限循环,JavaScript 引擎等待任务,执行它们然后休眠,等待更多任务

任务被设置 - 引擎处理它们 - 然后等待更多任务(同时休眠并消耗接近零的 CPU)。可能会发生在引擎繁忙时任务到来,然后将其排队的情况。任务形成一个队列,即所谓的“宏任务队列

微任务完全来自我们的代码。它们通常由Promise创建: .then/catch/finally 处理程序的执行成为一个微任务。微任务也在 await 的“掩护下”使用,因为它是Promise处理的另一种形式。在每个宏任务之后,引擎会立即执行微任务队列中的所有任务,然后再运行任何其他宏任务或渲染或其他任何东西

在此处输入图片说明

你刚刚从javascript.info/event-loop 中撕掉了这个答案
2021-03-14 14:58:10
2021-03-15 14:58:10