Task queue

Task Queue Is a task queue? It isn’t.

It’s a set, because instead of fetching tasks first in, first out like a queue, the oldest tasks are fetched, executed, and then deleted.

Are microtasks task queues? It isn’t.

Microtasks are, so to speak, adjunct to macro tasks, and macro task queues and microtask queues together form task queues

Why do we need to distinguish between macro and micro tasks? Can’t we just use one task queue?

Macro task

The main thread of the browser, which is the page process created for each page, is responsible for the following tasks, which can be used as macro tasks:

  • Render (parsing DOM, calculating layout, and drawing)
  • User interaction (click, drag, touch, zoom in and out)
  • JavaScript script execution
  • Network request complete (network process via IPC), file read and write complete, history API

How should these events or responses take place? In what order? This is where message queuing and event mechanisms are introduced

The renderer maintains multiple message queues, such as delay queues and regular message queues. The main thread uses a for loop to continually pull and execute tasks from these task queues. Any task in one of these message queues is called a macro task

So how do I get the message queue? Why isn’t task Queue a queue

  • The oldestTask, oldestTask, is selected from multiple message queues
  • The loop system records the start time of tasks and sets oldestTask as currentRunningTask
  • After the task is executed, currentRunningTask is set to null and the task is deleted from the task queue
  • Count how long it took for the task to complete (this is why the browser alarms when setTimeout does not call back at the preset time)

In fact, macro tasks already meet most of the requirements, and early browser implementations did not have microtasks when the hardware was not good enough. Macro task time granularity is large and the execution interval cannot be accurately controlled (settimeout example). In addition, with the improvement of hardware performance, the time precision of the task needs to be higher, which makes macro task difficult to perform. Macro task performance is lagging behind. Such as listening for DOM changes

Micro tasks

A microtask is defined as a function callback that is performed before the current macro task terminates (the only thing left in the context call stack is the window — this is checkpoint).

How microtasks work

When JS executes a script, V8 creates a global context for it, along with a queue of microtasks. During the execution of a function in the global context, if a microtask is created, it is put into the microtask queue. This queue is only accessible by the V8 engine, js cannot be accessed directly.

The timing of microtasks

There are two main ways

  • MutationObserver monitors changes to a DOM node, binds the callback, and then places the callback function in the microtask queue when the node is modified via JS (including adding or removing some molecular nodes)
  • Promise When promise.resolve () or promise.reject () is called, a microtask is put into the microtask queue (corresponding to the first and second arguments of then, and catch, respectively).

Checkpoint of microtask queue

When the js main function in the macro task has been executed and the JS engine is ready to exit the global execution context and clear the call stack, the JS engine now goes back to query the microtask queue. Then do it in order. In addition, there are other checkpoints (less important) for details. At this point, if a microtask is generated during the execution of the microtask, it will continue to be added to the microtask queue. V8 loops until the queue is empty.

For example

console.log(1)
setTimeout((a)= > {
    Promise.resolve(3).then(console.log)
    console.log(2)},1000)
Copy the code

Obviously, the print order is 1,2,3. Let’s analyze the execution

  1. Create the window’s global execution context
  2. Call console to print 1
  3. Call setTimeout to add the callback function to the delay queue and record the timing as 1s
  4. Wait 1s to remove the callback function from the task queue and execute it. This is a macro task
  5. Resolve, create the console.log microtask and place it in the microtask queue
  6. Printout 2
  7. The call stack is left with the global context, now check that the microtask queue is not empty and perform the microtask printout 3
  8. If the microtask is empty, the current macro task exits

A few things to note

  1. Microtasks are bound to macro tasks, and each macro task creates its own microtask
  2. The duration of microtask execution will affect the duration of the current red task. For example, one macro task creates 100 microtasks, and each task takes 10ms, so it can be said that the duration of the macro task is extended by 1000ms by executing the microtask. Therefore, attention must be paid to controlling the duration of the microtask execution
  3. Both microtasks and new macro tasks can be created within a macro task, but microtasks always precede macro task execution

The most used microtask is probably the one Promise created, but MutationOvserver is also worth mentioning, having had a somewhat troubled history. Its predecessor was MutationEvent, and the source of MutationEvent was the need to monitor DOM changes in real time (before there was no MutationEvent, setTimeout or setInterval was used for polling mechanism, but it could not achieve real-time performance). But MutationEvent is a synchronous callback, which means that if there is a DOM change, the callback will be called immediately, and the rendering engine needs to execute JavaScript immediately. For example, if a node is changed 10 times in a macro task, the 10 callback will be triggered. Each callback needs to be assumed to be 100ms, which is a demonstration of 1s, so if the browser is performing an animation, it will cause a lag.

Due to the performance problem of synchronous invocation, MutationEvent was eventually abandoned and replaced by MutationObserver. The great difference from MutationEvent is that the former is an asynchronous invocation. This way, for example, if you change a node 10 times in a macro task, you can command and trigger an asynchronous call once, so that even frequent DOM manipulation (which should be avoided, of course) is not a big performance problem. Should this asynchrony use macro tasks or microtasks? As mentioned above, due to real-time requirements, it is necessary to call back and execute as soon as possible after DOM changes, so it is suitable for microtasks. Therefore, MutationObserver adopts the strategy of “asynchronous + microtasks” to solve the performance problem of blocking main thread asynchronously, and microtasks to solve the real-time problem.

test

Finally, a piece of code, I was asked in the interview

function executor(resolve, reject) {
    let rand = Math.random()
    console.log(1)
    if(rand > 0.5) resolve()
    reject()
}
const p0 = new Promise(executor)
const p1 = p0.then(_= > {
    console.log('success-0')
    return new Promise(executor)
})
const p2 = p1.then(_= > {
    console.log('success-1')
    return new Promise(executor)
})
p2.catch(_= >{
    console.log('error')})console.log(2)
Copy the code

Note that all of the above processes are performed within the same macro task, and the answer is not unique, but as long as you understand microtasks, you can basically explain it.