Search code examples
javascriptasynchronousw3cvm-implementation

Javascript API to explicitly add micro tasks or macro tasks


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:

  • A VM 'turn' is the fact of pulling ONE macro task out of the VM macro task queue, and execute it.
  • During a VM turn, micro tasks can be added to the micro tasks queue of the current macro task.
  • Micro tasks can push other micro tasks to the micro tasks queue of the current macro task.
  • A VM turn will end when the micro task queue is empty.

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 !


Solution

  • 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'));