JavaScript API可以明确添加微任务或宏任务

6
从我对JavaScript虚拟机的全局了解来看,我清楚地看到“微任务/宏任务”的概念起着重要作用。
这是我对此的理解:
- VM“轮”是从VM宏任务队列中取出一个宏任务并执行的过程。 - 在VM轮期间,可以将微任务添加到当前宏任务的微任务队列中。 - 微任务可以将其他微任务推入当前宏任务的微任务队列。 - 当微任务队列为空时,VM轮将结束。
我的问题是: 为什么没有明确的API来操作这两个队列。 像这样的东西
- pushToMacroTask(function) - pushToMicroTask(function)
事实上,似乎操纵这些队列的唯一方法是使用setTimeout()将任务添加到宏任务队列,并使用Promises将任务添加到微任务队列...
我对此没有意见,但这并没有给我们一个有意义的API,你不觉得吗?
这个概念是否应该对JS dev保持“隐藏”,只在某些hacky情况下使用?
你知道是否有任何W3C规范涉及这个主题吗?
所有VM引擎是否都以相同的方式实现了这个概念?
我很高兴听到关于这个问题的故事和意见。

实际上,不同的事件源有多个宏任务队列。 - Bergi
你是什么意思?能详细解释一下吗?谢谢。 - Clement
是的,我认为这应该是一项实现细节。虚拟机应该自己优化调度,适应不同的用例(Promises、Timeouts、DOM 事件、网络事件等)。如果您需要控制此过程,请编写自己的调度程序。 - Bergi
1个回答

11

有关微任务/宏任务,是否存在W3C规范?

W3C提到了 任务队列:

当一个用户代理要排队执行任务时,必须将给定的任务添加到相关事件循环的其中一个任务队列中。同一特定任务源(例如由计时器生成的回调、为鼠标移动分派的事件、为解析器排队的任务)的所有任务必须始终添加到同一任务队列中,但来自不同任务源的任务可能会被放置在不同的任务队列中。

EcmaScript2015提到了作业队列,并要求至少支持两种:

  • ScriptJobs: 用于验证和评估ECMAScript脚本和模块源文本的作业。
  • PromiseJobs: 是对Promise结算的响应的作业。

这个语言定义忽略了可能存在的事件循环,但是可以想象将保留一个或多个作业队列供W3C规范中提到的任务队列使用。浏览器将按照W3C任务队列规范触发setTimeout回调——链接到作业队列,而Promise 必须直接使用作业队列规范(而不是任务队列)。这里还提到了代理可以向作业队列注入任务的可能性:

或者,[实现]可能选择等待某种特定实现的代理或机制来排队新的PendingJob请求。

EcmaScript规范不强制执行为不同的作业队列提供服务的优先级:

本规范未定义多个作业队列的服务顺序。ECMAScript实现可以将一个作业队列的PendingJob记录的FIFO评估与其他一个或多个作业队列的PendingJob记录评估交织在一起。

因此,似乎没有严格的要求在setTimeout任务之前必须服务于Promise履行。但是Web超文本应用技术工作组[WHATWG]在涵盖事件循环时更具体:

每个事件循环都有一个微任务队列。微任务是最初要排队到微任务队列而不是任务队列的任务。

[2019补充]: 在此期间,存活的HTML标准 [WHATWG] 中现在包括以下内容:

8.6 Microtask queuing

self.queueMicrotask(callback)

Queues a microtask to run the given callback.

The queueMicrotask(callback) method must queue a microtask to invoke callback, and if callback throws an exception, report the exception.

The queueMicrotask() method allows authors to schedule a callback on the microtask queue. This allows their code to run after the currently-executing task has run to completion and the JavaScript execution context stack is empty, but without yielding control back to the event loop, as would be the case when using, for example, setTimeout(f, 0).

所有虚拟机引擎实现方式一样吗?

历史上,不同浏览器的实现导致执行顺序不同。这篇来自2015年的文章可能是一个有趣的阅读,可以了解它们之间的差异:

一些浏览器[...]在setTimeout之后运行promise回调函数。他们很可能将promise回调作为新任务的一部分而不是微任务。

FirefoxSafari在单击监听器之间正确地耗尽了微任务队列,如突变回调所示,但是promise似乎被以不同的方式排队。[...] 对于Edge,我们已经看到它错误地排队promise,但也未能在调用所有侦听器后排空微任务队列。

从那时起,已经解决和协调了几个问题。

请注意,不必存在一个微任务队列或一个宏任务队列。可以有几个队列,每个队列都有自己的优先级。

有意义的API

当然,实现你建议的两个函数并不是那么困难:

let pushToMicroTask = f => Promise.resolve().then(f);
let pushToMacroTask = f => setTimeout(f);

pushToMacroTask(() => console.log('Macro task runs last'));
pushToMicroTask(() => console.log('Micro task runs first'));

[2019]现在我们有了queueMicrotask(),它是一种本地实现。以下是一个演示,比较这种方法与上面基于Promise的实现:

let queuePromisetask = f => Promise.resolve().then(f);
let queueMacrotask= f => setTimeout(f);

queueMicrotask(() => console.log('Microtask 1'));
queueMacrotask(() => console.log('Macro task'));
queuePromisetask(() => console.log('Promise task'));
queueMicrotask(() => console.log('Microtask 2'));


1
new Promise(f) is as good as f(). You meant Promise.resolve().then(f) - Bergi
感谢您的解释! - Clement

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接