【问题标题】:Difference between microtask and macrotask within an event loop context事件循环上下文中微任务和宏任务之间的区别
【发布时间】:2014-11-12 23:15:41
【问题描述】:

我刚刚读完 Promises/A+ 规范,偶然发现了微任务和宏任务这两个术语:请参阅 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-queue 中所述 和https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint

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

【问题讨论】:

  • 简而言之:多个嵌套事件队列。你甚至可以自己实现一个:while (task = todo.shift()) task();
  • 对于想要了解更多细节的人:JavaScript Ninja 的秘密,第 2 版,第 13 章幸存事件

标签: javascript node.js promise event-loop


【解决方案1】:

事件循环的一次复飞将从宏任务队列处理恰好一个任务(此队列简称为任务队列 em> 在WHATWG specification)。 在这个宏任务完成后,所有可用的微任务都将被处理,即在同一个复飞周期内。在处理这些微任务时,它们可以将更多微任务排队,这些微任务将全部运行,直到微任务队列耗尽。

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

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

然而,至少关于 Node.js 的 process.nextTick 函数(将 microtasks 排队),通过 process.maxTickDepth 可以防止这种阻塞。此值设置为默认值 1000,在达到此限制后减少对 微任务 的进一步处理,从而允许处理下一个 宏任务

那么什么时候用什么?

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

示例

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

【讨论】:

  • 虽然在事件循环中有一个微任务检查点,但这并不是大多数开发人员会遇到微任务的地方。当 JS 堆栈清空时处理微任务。这可能在一个任务中发生多次,甚至在事件循环的渲染步骤中发生。
  • process.maxTickDepth 很久以前就被删除了:github.com/nodejs/node/blob/…
  • 也可以使用queueMicrotask()方法添加新的微任务
  • requestAnimationFrame(rAF) 不仅生成微任务。一般来说,rAF 调用会创建一个separate queue
  • Jake Archibald 的这篇文章帮助我理解了其中的区别:jakearchibald.com/2015/tasks-microtasks-queues-and-schedules
【解决方案2】:

spec中的基本概念:

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

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

call stack为空时,执行步骤-

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

简化的流程模型如下:

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

要记住的事情:

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

【讨论】:

  • 这是很好的解释!感谢分享!。还有一点要提的是在 NodeJs 中,setImmediate() 是宏/任务,process.nextTick() 是微/作业。
  • 浏览器paint任务呢?他们适合属于哪个类别?
  • 我认为他们适合微任务(如requestAnimationFrame
  • 我不知道我是不是弄错了,但我有点不同意这个答案,微任务在宏任务之前运行。 codepen.io/walox/pen/yLYjNRq?
  • @walox 当前脚本执行也是一个宏任务。一旦所有同步代码完成,事件循环将优先考虑微任务而不是宏任务。与您的示例一样,脚本执行后,超时回调在宏任务/回调队列中,承诺回调在微任务队列中。由于一个宏任务已经完成(主脚本执行),事件循环将优先承诺任务而不是超时任务。因此结果。
【解决方案3】:

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

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]");
/* 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 */

【讨论】:

  • 将队列称为堆栈完全令人困惑。
  • 感谢关于 Node.js 的旁注。
【解决方案4】:

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

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

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

任务已设置——引擎会处理它们——然后等待更多任务(在睡眠期间并消耗接近于零的 CPU)。 可能会发生在引擎忙时任务来了,然后它被排队了。 这些任务形成一个队列,即所谓的“宏任务队列

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

【讨论】:

猜你喜欢
  • 2019-01-31
  • 1970-01-01
  • 2021-06-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-06-22
  • 1970-01-01
相关资源
最近更新 更多