From my global understanding of how javascript virtual machines works, i can clearly see that the concept of micro task / macro task play a big role.
Here is what i understand about that:
And here is the point of my question:
Why there is no clear API to manipulate those two queues.
Something like
pushToMacroTask( function )
pushToMicroTask( function )
Actually it seams like the only way to manipulate those queue is to use setTimeout()
to add tasks to the macro tasks queue and Promises
to add tasks to the micro tasks queue...
I'm ok with that but this does not give us a meaningfull API, don't you think ?
Is this concept supposed to remain 'hidden' to JS dev and only used in some hacky situations ?
Do you know if there is any W3C spec around that subject ?
Does all VM engines implement this concept the same way ?
I'd be glad to ear stories and opinions about that.
Thanks !
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 invokecallback
, and ifcallback
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)
.
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.
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'));