Is there is any W3C spec concerning micro/macro tasks?
W3C speaks of task queues:
When a user agent is to queue a task, it must add the given task to one of the task queues of the relevant event loop. All the tasks from one particular task source (e.g. the callbacks generated by timers, the events dispatched for mouse movements, the tasks queued for the parser) must always be added to the same task queue, but tasks from different task sources may be placed in different task queues.
EcmaScript2015 speaks of Job Queues, and requires that at least two are supported:
- ScriptJobs: Jobs that validate and evaluate ECMAScript Script and Module source text.
- PromiseJobs: Jobs that are responses to the settlement of a Promise.
This language definition is ignorant of a possible event loop, but one can imagine one or more Job Queues being reserved for use with the Task Queues mentioned in the W3C specs. A browser will trigger the setTimeout
callback according to the W3C Task Queue specification -- linked to a Job Queue --, while a promise must use the Job Queue specification directly (not the Task Queue). That an agent could inject tasks into a Job Queue is mentioned as well:
Alternatively, [an implementation] might choose to wait for a some implementation specific agent or mechanism to enqueue new PendingJob requests.
The EcmaScript specs do not enforce a priority for servicing different Job Queues:
This specification does not define the order in which multiple Job Queues are serviced. An ECMAScript implementation may interweave the FIFO evaluation of the PendingJob records of a Job Queue with the evaluation of the PendingJob records of one or more other Job Queues.
So, there seems no strict requirement here that promise fulfillments should be serviced before setTimeout
tasks. But the Web Hypertext Application Technology Working Group [WHATWG] is more specific when covering event loops:
Each event loop has a microtask queue. A microtask is a task that is originally to be queued on the microtask queue rather than a task queue.
[2019 addition]: In the mean time the Living HTML Standard [WHATWG] now includes the following:
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)
.
Do all VM engines implement this the same way?
Historically, different browser's implementations lead to different orders of execution. This article from 2015 might be an interesting read to see how different they were:
Some browsers [...] are running promise callbacks after setTimeout
. It's likely that they're calling promise callbacks as part of a new task rather than as a microtask.
Firefox and Safari are correctly exhausting the microtask queue between click listeners, as shown by the mutation callbacks, but promises appear to be queued differently. [...] With Edge we've already seen it queues promises incorrectly, but it also fails to exhaust the microtask queue between click listeners, instead it does so after calling all listeners.
Since then several issues have been solved and harmonised.
Note however that there does not have to be one micro task queue, nor one macro task queue. There can be several queues, each with their own priority.
A meaningful API
It is of course not so difficult to implement the two functions you suggested:
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] And now that we have queueMicrotask()
, there is a native implementation. Here is a demo comparing that method with the Promise-based implementation above:
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'));