Outline of the answer

  1. Say basic knowledge first, macro task, micro task have what
  2. Talk about the event loop process, and draw it as you go
  3. Said async/await execution order note that can turn a chrome optimization, it is illegal to the specification, V8 team PR these confident, seems you are very studious, understand very detailed, very clear.
  4. Repeat points 1, 2, and 3. Point 3 in Node is the point where the event loop changes before and after Node11.

Let’s follow the outline and talk about each point

Event loops in the browser

JavaScript code relies on the function call stack to determine the order of execution of functions, and on the task queue to determine the execution of other code. The entire execution process is called the event loop process. Event loops are unique in a thread, but task queues can have multiple. The task queue is further divided into macro-tasks and micro-tasks, which are called tasks and jobs in the latest standards.

Macro-task may include:

  • Script (overall code)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

A micro-task may include:

  • process.nextTick
  • Promise
  • Async/Await(actually promise)
  • MutationObserver(new HTML5 feature)

Overall implementation, I drew a flowchart:

The general conclusion is that the macro task is executed, and then the microtask generated by the macro task is executed. If a new microtask is generated during the execution of the microtask, the microtask will continue to be executed. After the execution of the microtask is completed, the microtask will return to the macro task for the next round of cycle. Here’s an example:

Async2 end => Promise => async1 end => promise1 => promise2 => setTimeout As follows:

Async /await execution order

We know that async implicitly returns a Promise as a result, so the simple idea is that when the function following the await completes, the await will generate a microtask (promise.then is a microtask). However, we should pay attention to the timing of the microtask. It is generated by executing the await and immediately jumping out of the async function to execute other code (here is the coroutine operation, A pauses execution, B gives control). After the rest of the code is finished, return to the async function to execute the rest of the code and register the code following the await in the microtask queue. Let’s look at an example:

console.log('script start')

async function async1() {
await async2()
console.log('async1 end')}async function async2() {
console.log('async2 end')
}
async1()

setTimeout(function() {
console.log('setTimeout')},0)

new Promise(resolve= > {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')})console.log('script end')
 // The old version of the output is as follows, but please keep reading at the bottom of this article to note that there are changes in the new version
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
Copy the code

Analyze this code:

  • Execute the code, outputscript start.
  • Async1 () is executed, async2() is called, and then outputasync2 endThe async1 function is left in context and the async1 function is skipped.
  • When setTimeout is encountered, a macro task is generated
  • Execute the Promise, outputPromise. When then is encountered, the first microtask is generated
  • Continue executing the code, outputscript end
  • After the code logic is executed (the current macro task is executed), the microtask queue generated by the current macro task is executed and outputpromise1When the microtask encounters then, a new microtask is generated
  • Execute the resulting microtask, outputpromise2, the current microtask queue is completed. Execute the power back to async1
  • Performing an await actually results in a promise return, i.e
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
Copy the code

When the execution is complete, execute the statement after await with the output async1 end

  • Finally, the next macro task, which executes setTimeout, is executed to outputsetTimeout

Pay attention to

In the new version of Chrome, it is not printed as above, because Chrome has been optimized, and await is now faster. The output is:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
Copy the code

But this practice is actually against the specification, of course the specification can be changed, this is a PR of the V8 team, the new version of the print has been modified. Also has a discussion on zhihu, can take a look at www.zhihu.com/question/26…

We can understand it in two ways:

  1. If await is directly followed by a variable, such as: await 1; This is equivalent to directly registering the code following the await as a microtask, which can be simply read as promise.then(code below the await). When the promise function is encountered, the promise.then() function is registered to the microtask queue. Note that the microtask queue already contains the “await” microtask. So in this case, the code after the await (async1 end) is executed, followed by the microtask code registered after the async1 function (PromisE1, Promise2).

  2. If the await is followed by an asynchronous function call, as in the above code, change the code to look like this:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')}async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(() = >{
        console.log('async2 end1')
    })
}
async1()

setTimeout(function() {
    console.log('setTimeout')},0)

new Promise(resolve= > {
    console.log('Promise')
    resolve()
})
.then(function() {
    console.log('promise1')
})
.then(function() {
    console.log('promise2')})console.log('script end')
Copy the code

The output is:

// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
Copy the code

At this point, awit does not register the code following the await in the microtask queue. Instead, after executing the await, it jumps out of the async1 function and executes other code. Then when you come across a promise, register promise.then as a microtask. After the rest of the code is completed, we need to go back to the async1 function to execute the rest of the code and register the code following the await in the microtask queue. Note that there are previously registered microtasks in the microtask queue. In this case, a microtask outside of async1 will be executed first (promise1, Promise2), and then a microtask registered in async1 will be executed (async1 end). It can be understood that in this case, the code after the await is executed at the end of the round. There are event loops in browsers as well as in nodes. Event loops are the mechanism by which Nodes handle non-blocking I/O operations. The implementation of event loops in nodes relies on the Libuv engine. Since some of the principles of the event loop have changed since Node 11, here’s the new standard, with the change points listed at the end to give you an idea of why.

Event loop in Node

There are event loops in browsers as well as in nodes. Event loops are the mechanism by which Nodes handle non-blocking I/O operations. The implementation of event loops in nodes relies on the Libuv engine. Since some of the principles of the event loop have changed since Node 11, here’s the new standard, with the change points listed at the end to give you an idea of why.

Macro tasks and microtasks

Node also has macro tasks and microtasks, similar to event loops in browsers, where,

Macro-task may include:

  • setTimeout
  • setInterval
  • setImmediate
  • Script (overall code)
  • I/O operations.

A micro-task may include:

  • Process. nextTick(unlike normal microtasks, executed before the execution of the microtask queue)
  • New Promise().then(callback), etc.

Overall understanding of the Node event loop

Here’s a simplified diagram of the Node event loop from the official website:

Each box in the diagram is called a phase of the event loop mechanism, and each phase has a FIFO queue to perform a callback. While each phase is special, typically, when the event loop enters a given phase, it performs any operation specific to that phase, and then executes the callbacks in the queue for that phase until the queue runs out or the maximum number of callbacks has been executed. When the queue is exhausted or reaches the callback limit, the event loop moves to the next stage.

Therefore, from the simplified diagram above, we can analyze the sequence of node event loops as follows:

Incoming data -> Poll -> Check -> Close callback -> Timers ->I/O event callback Callbacks -> idle -> prepare…

Summary of stage

  • Timers: The callback functions of setTimeout and setInterval are executed during the timer detection phase.
  • I/O event callbacks: I/O callbacks that are delayed until the next iteration of the loop, i.e. those I/O callbacks that were not executed in the previous loop.
  • Idle (prepare) : Used only by the system.
  • Poll: New I/O events are retrieved. Performing I/ O-related callbacks (in almost all cases, except for the closed callbacks, those scheduled by timers and setImmediate()), node will block here at the appropriate time.
  • Check: The setImmediate() callback is executed here
  • Close callback: Some closed callback functions, such as socket.on(‘close’,…) .

Three key stages

Most asynchronous tasks in daily development are handled in poll, Check, and Timers, so let’s focus on these three phases.

timers

The Timers phase performs setTimeout and setInterval callbacks and is controlled by the poll phase. Similarly, in Node, the time specified by the timer is not the exact time, it must be executed as soon as possible.

poll

Poll is a crucial stage. The execution logic flowchart of poll stage is as follows:

If a timer is running out of time, the eventLoop returns to the Timers phase.

If there is no timer, it will look at the callback queue.

  • If the poll queue is not empty, the callback queue is traversed and executed synchronously until the queue is empty or the system limit is reached

  • If the poll queue is empty, two things happen

    • If a setImmediate callback needs to be performed, the poll phase is stopped and the check phase is performed
    • If no setImmediate callback needs to be performed, it waits for the callback to be added to the queue and executes the callback immediately, again with a timeout set to prevent waiting, and then automatically goes to check after a certain amount of time.
check

The check phase. This is a relatively simple phase that directly executes the callback to setImmdiate.

process.nextTick

Process. nextTick is a task queue independent of eventLoop.

After each eventLoop phase is completed, the nextTick queue is checked and, if there are any tasks in it, they are given priority over microtasks.

Here’s an example:

setImmediate(() = > {
    console.log('timeout1')
    Promise.resolve().then(() = > console.log('promise resolve'))
    process.nextTick(() = > console.log('next tick1'))}); setImmediate(() = > {
    console.log('timeout2')
    process.nextTick(() = > console.log('next tick2'))}); setImmediate(() = > console.log('timeout3'));
setImmediate(() = > console.log('timeout4'));
Copy the code
  • Before Node11, because every eventLoop phase checks the nextTick queue, and if there’s a task in it, it lets that task execute before the microtask, so the code goes to the check phase and performs all setImmediate, The nextTick queue is executed after completion, and the microtask queue is executed finally, so the output istimeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
  • After Node11, process.nextTick is a kind of microtask, so the above code goes into the check phase, executes a setImmediate macro task, then executes its microtask queue, then executes the next macro task and its microtasks, so the output istimeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

Description of node version differences

The main difference here is the difference before and after Node11, because some of the features after Node11 are now in line with the browser, and the overall change in a word, If the node11 version is used to perform a macro task (setTimeout,setInterval, and setImmediate) in a phase, a queue of microtasks is performed at once

Changes in the timing of the Timers phase

setTimeout(() = >{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')})},0)
setTimeout(() = >{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')})},0)
Copy the code
  • If the Node11 version performs a macro task in a phase (setTimeout,setInterval, and setImmediate), the microtask queue is executed instantaneously, just as it does on the browser side, and the end result istimer1=>promise1=>timer2=>promise2
  • For node10 and earlier versions, the first timer is executed and the second timer is in the completion queue.
    • If the second timer is not in the completion queue, the final result istimer1=>promise1=>timer2=>promise2
    • If the second timer is already in the completion queue, the final result istimer1=>timer2=>promise1=>promise2

The execution time of the check phase changes

setImmediate(() = > console.log('immediate1'));
setImmediate(() = > {
    console.log('immediate2')
    Promise.resolve().then(() = > console.log('promise resolve'))}); setImmediate(() = > console.log('immediate3'));
setImmediate(() = > console.log('immediate4'));
Copy the code
  • If the version is later than Node11, outputimmediate1=>immediate2=>promise resolve=>immediate3=>immediate4
  • If the version is before Node11, outputimmediate1=>immediate2=>immediate3=>immediate4=>promise resolve

The execution timing of the nextTick queue changed

setImmediate(() = > console.log('timeout1'));
setImmediate(() = > {
    console.log('timeout2')
    process.nextTick(() = > console.log('next tick'))}); setImmediate(() = > console.log('timeout3'));
setImmediate(() = > console.log('timeout4'));
Copy the code
  • If the version is later than Node11, outputtimeout1=>timeout2=>next tick=>timeout3=>timeout4
  • If the version is before Node11, outputtimeout1=>timeout2=>timeout3=>timeout4=>next tick

Just a few examples above, and you should see the difference, but keep in mind that if the Node11 version performs a macro task (setTimeout,setInterval, and setImmediate) in a phase, the queue of microtasks is executed immediately.

Key differences between Node and browser eventLoop

The main difference between the two is that microtasks in the browser are performed within each corresponding macro task, whereas microtasks in NodeJS are performed between phases.

More understanding information

  • 【 speech solution series 】 Talk about JS event loop mechanism (including full score answer skills)
  • 【 speech solution series 】 Talk about JS event loop mechanism (including full score answer skills)
  • Self-test questions

The resources

  • The front end of the interview
  • Consider the Node event loop triggered by an interview question
  • Node.js event loops, timers and Process.nexttick ()
  • Detail the Event Loop mechanism in JavaScript
  • New Changes to the Timers and Microtasks in Node V11.0.0 (and above)

The last

  • Welcome to add my wechat (Winty230), pull you into the technology group, long-term communication and learning…
  • Welcome to pay attention to “front end Q”, seriously learn front end, do a professional technical person…