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


140

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

我以前从未听说过这些术语,现在我很好奇可能会有什么不同?

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

另外,我发现了一个名为“ macrotask”的npm模块: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规范,我知道理论上我应该可以自己提取差异。但是我确信,专家的简短解释也可以使其他人受益。


简而言之:多个嵌套事件队列。您甚至可以自己实施:while (task = todo.shift()) task();
Bergi 2014年

1
对于需要更多细节的人:JavaScript忍者的秘密,第二版,第13章生存事件
Ethan,

Answers:


220

事件循环的一种解决方法是从宏任务队列中恰好处理一个任务(在WHATWG规范中,该队列简称为任务队列)。此宏任务完成后,将处理所有可用的任务,即在相同的复现周期内。在处理这些微任务时,它们可以将更多的微任务排入队列,所有这些微任务将一一运行,直到用完微任务队列。

这有什么实际后果?

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

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

那么什么时候使用什么呢?

基本上,当您需要以同步方式异步执行事务时(例如,您说要在最近期内执行此(微)任务),请使用微任务。否则,请坚持宏任务

例子

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


4
尽管事件循环中有一个微任务检查点,但这不是大多数开发人员会遇到微任务的地方。JS堆栈为空时,将处理微任务。这可能在任务中甚至事件循环的渲染步骤中多次发生。
JaffaTheCake

2
process.maxTickDepth很久以前被删除:github.com/nodejs/node/blob/…–
RidgeA

您还可以使用queueMicrotask()方法添加新的微任务
ZoomAll

感谢@ZoomAll,直到现在才知道queueMicrotask()。我已将其添加到答案以及所有内容的链接...
NicBright

requestAnimationFrame(rAF)不仅生成微任务。一般来说,rAF呼叫会创建一个单独的队列
ZoomAll

67

规格中的基本概念:

  • 一个事件循环具有一个或多个任务队列。(任务队列是宏任务队列)
  • 每个事件循环都有一个微任务队列。
  • 任务队列=宏任务队列!=微任务队列
  • 一个任务可能被推入宏任务队列或微任务队列
  • 当一个任务被放入队列(微型/宏)时,表示准备工作已经完成,因此该任务现在可以执行。

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

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

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

简化的过程模型如下:

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

要记住的事情:

  1. 当一个任务(在宏任务队列中)正在运行时,可能会注册新事件。因此可能会创建新任务。下面是两个新创建的任务:
    • promiseA.then()的回调是一项任务
      • promiseA已解决/被拒绝:任务将在当前事件循环中被推送到微任务队列中。
      • promiseA待定:任务将在事件循环的下一轮(可能是下一轮)中被推送到微任务队列中
    • setTimeout(callback,n)的回调是一个任务,将被推送到宏任务队列中,即使n为0;
  2. 微任务队列中的任务将在本轮运行,而宏任务队列中的任务必须等待下一轮事件循环。
  3. 我们都知道“ click”,“ scroll”,“ ajax”,“ setTimeout” ...的回调是任务,但是我们还应该记住脚本标记中的js代码整体上也是一个任务(宏任务)。

2
这是很好的解释!感谢分享!。还有一点要提到的是NodeJs,它 setImmediate()是宏/任务,process.nextTick()是微任务。
LeOn-韩立

6
那浏览器paint任务呢?他们适合哪个类别?
传奇

我认为它们适合微任务(例如requestAnimationFrame
Divyanshu Maithani

这是v8事件循环运行的顺序->调用堆栈|| 微型任务|| 任务队列|| 皇家空军|| 渲染树|| 布局|| 油漆|| <OS Native调用以在屏幕上绘制像素> <----- 1)根据事件循环计时器在requestAnimationFrame回调之后发生DOM(新更改),CSSOM(新更改),渲染树,布局和绘制。这就是为什么在rAF之前尽可能多地完成DOM操作非常重要的原因,剩下的就可以在rAF中进行。PS:调用rAF将触发宏任务执行。
Anvesh Checka

我不知道我是否记错了,但我有点不同意这个答案,微任务先于宏任务运行。codepen.io/walox/pen/yLYjNRq
walox

9

我认为我们不能讨论将事件循环从堆栈中分离出来,因此:

JS具有三个“堆栈”:

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

事件循环是这样工作的:

  • 从堆栈的底部到顶部执行所有操作,仅当堆栈为空时,检查上述队列中的情况
  • 检查微堆栈并在堆栈的帮助下执行其中的所有内容(如果需要),一个接一个地执行微任务,直到微任务队列为空或不需要执行任何操作,然后仅检查宏堆栈
  • 检查宏堆栈并在堆栈的帮助下执行所有宏(如果需要)

如果堆栈不为空,则不会触摸Mico堆栈。如果微堆栈不为空或不需要执行,则宏堆栈不会被触摸。

综上所述:微任务队列与宏任务队列几乎相同,但是那些任务(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]");

/* Result:
stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4]
micro [6]
stack [4]
micro [6]
stack [4]
micro [6]

macro [5], macro [5], macro [5]
--------------------
but in node in versions < 11 (older versions) you will get something different


stack [1]
macro [8]

stack [7], stack [7], stack [7]

macro [2]
macro [3]

stack [4], stack [4], stack [4]
micro [6], micro [6], micro [6]

macro [5], macro [5], macro [5]

more info: https://blog.insiderattack.net/new-changes-to-timers-and-microtasks-from-node-v11-0-0-and-above-68d112743eb3
*/

1
将队列称为堆栈完全令人困惑。
Bergi
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.