Start with an interview question

console.log(1)

setTimeout(function() {
  console.log(2)})new Promise(function (resolve) {
  console.log(3)
  resolve()
 }).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)})console.log(6)
Copy the code

Start by using your existing knowledge: What is the output of the code above?

The answers are: 1, 3, 6, 4, 5, 2

If you can accurately answer the above questions and state your evidence, then congratulations — you have a solid foundation in the event loop and can skip to the actual part of the test. If your answer is different, don’t worry, the output order is determined by the browser’s event loop rules. That’s what we’re gonna do next

Event-loop parsing in the browser

Key Role Analysis

In the browser event loop, you need to recognize three roles: function call stack, macro-task queue, and micro-task queue.

Function call stack (described in Execution Context and Call Stack) : When the engine encounters JS code for the first time, a global execution context is generated and pushed onto the call stack. Each subsequent function call pushes a new function context onto the stack. The JS engine will execute the function at the top of the stack, and when the execution is complete, the corresponding context pops up.

In short: If you have a bunch of logic that needs to be executed, it needs to be pushed onto the function call stack first and executed later. The function call stack is the place to do the work, and it will actually do the work for you.

What about macro and micro task queues?

As you know, JS features are single thread + asynchronous. In JS, we have some tasks, like the setTimeout task above, or the then task you put in a Promise — these tasks are asynchronous, they don’t need to be executed immediately, so when they are first dispatched, Does not “qualify” to enter the call stack.

This is not qualified to do what?

Wait in line!

So these tasks to be executed, according to certain rules, obediently queue up, waiting for the time to be pushed into the call stack arrival — this queue, is called the task queue.

The so-called “macro tasks” and “micro tasks” are further subdivisions of tasks. The specific division is as shown in the figure:

Common macro-tasks include setTimeout, setInterval, setImmediate, Script, I/O, etc

Common micro-tasks include Process. nextTick, Promise, MutationObserver, etc

Note: Script is also a macro task; SetImmediate in macro tasks and Process. nextTick in micro tasks are unique to Node.

Cyclic process interpretation

Based on the cognition of Micro and Macro, let’s go through the complete event cycle.

A complete Event Loop process can be summarized as the following stages:

  1. Execute and queue a macro-task. Note if it is in the initial state: call stack empty. The micro queue is empty, and the Macro queue has one and only one script. The script script is the first to execute and exit the queue;
  2. The global context (script tag) is pushed onto the call stack, synchronizing code execution. During execution, new Macro-tasks and micro-tasks can be created by calling some interfaces, which are pushed to their respective task queues. This process is essentially the process of queue macro task execution and queuing.
  3. In the previous step, we assigned a macro-task. In this step, we dealt with a micro-task. But it’s important to note that when Macro-Task is out, tasks are executed one by one; Micro-tasks, on the other hand, are executed in teams. So, as we process the micro queue, we execute the tasks in the queue one by one and de-queue them until the queue is empty;
  4. Perform rendering operations and update the interface;
  5. Check whether Web worker tasks exist and process them if so.

The five steps I’ve outlined here are a relatively complete process. In fact, for the interview, let’s focus on steps 1-3 is enough. Step 4 step 5, the interview said yes, do not say no one will be difficult for you, do not have to fight.

Rework the problem line by line

Now let’s do the question from the beginning again based on our understanding of the process:

console.log(1)

setTimeout(function() {
  console.log(2)})new Promise(function (resolve) {
  console.log(3)
  resolve()
 }).then(function () {
  console.log(4)
}).then(function() {
  console.log(5)})console.log(6)
Copy the code

The first thing that gets pushed onto the call stack is the global context. You can also think of the script as a macro task that gets pushed onto the call stack. This action also creates the global context. At the same time, the macro task queue is emptied and the microtask queue remains temporarily empty

The global code starts executing and runs through the first console:

console.log(1)
Copy the code

Then setTimeout is executed. A macro task is dispatched, and the macro task queue has a new sibling

Further down, I met a new Promise. As you know, the code for the body of the Promise constructor executes immediately, so that part of the logic executes

console.log(3)
resolve()
Copy the code

The first step outputs 3, and the second step determines that the state of the Promise is Fullfilled, which successfully pushes the corresponding two tasks in the then method into the microtask queue

Further down, you come to the last sentence of the global code

console.log(6)
Copy the code

This step prints 6, and the synchronization code in the script is finished. Note, however, that the global context doesn’t disappear — it lives and dies with the page itself. Next, let’s start pushing asynchronous tasks onto the call stack. Following the principle of “one macro, one micro”, we now need to process all tasks in the microtask queue

First comes the first callback registered in THEN, which prints 4

function () {
  console.log(4)}Copy the code

The second callback is then processed

This callback is going toThe output of 5

function () {
  console.log(5)}Copy the code

As a result, the microtask queue is emptied

Let’s refocus on the macro task queue and push one task at the head of its queue

The corresponding callback executes with output 2

function() {
  console.log(2)}Copy the code

When we’re done, we’re done processing all the tasks, and both task queues are empty

At this point, there is only one global context left, which will be destroyed when you close the TAB.

How is event-loop in Node different from the browser?

This is a popular interview question among big company interviewers. To understand the difference between event-loops in Node and browsers, you need to be able to explain the event-loop in Node itself.

Node technical architecture analysis – Understanding Libuv

Here I’ve drawn a simplified Node architecture for you

Node as a whole consists of three parts:

  • Application layer: This layer is the most familiar Node.js code, including the Node application and some of the standard libraries.
  • Bridge layer: Node layer is implemented in C++. The bridge layer is responsible for the ability to encapsulate the underlying C++ dependent modules, reducing them to apis that provide services to the application layer.
  • Low-level dependencies: this is the lowest level C++ library, where the most basic capabilities that support Node run converge. The ones that deserve special attention are V8 and Libuv:
    • V8 is the JS runtime engine that converts JavaScript code to C++ and then runs the C++ layer.
    • Libuv: It encapsulates cross-platform asynchronous I/O capabilities and is the focus of this article: event loops in Node are initialized by Libuv.

Note: The first difference here is that the event-loop of the browser is implemented by the browser itself; Node event-loop is implemented by Libuv.

The event-loop implementation in libuv

Libuv’s dominant loop has six phases. Here I quote Node official (source: nodejs.org/zh-cn/docs/…

Note: The official Node graph is worth referring to, but it is not recommended that you read the official Node documentation to understand the event loop. Some of the expressions are still somewhat awkward and discouraging.

Let’s take a look at what each of the six stages deals with:

  • Timers phase: performsetTimeoutsetIntervalThe callback defined in
  • Pending callbacks: “Pending callbacks.” If an error occurs during network /O or file I/O, the pending callbacks are handled at this stage (which is rare and can be skipped).
  • Idle, prepare: used only in the system. We developers don’t have to worry about this phase. (can be skipped);
  • Poll: Key stage. This stage performs I/O callback and checks whether the timer expires.
  • Check: Handles the callbacks defined in setImmediate;
  • close callbacks: Handles some “closed” callbacks, such assocket.on('close', ...)It’s going to be triggered at this point.

Macro and micro tasks

Just like in browsers, there are macro and micro tasks in the Node world. The basis of division is consistent with what we described above

  • Common macro-tasks include setTimeout, setInterval, setImmediate, Script, I/O, UI rendering, etc.
  • Common micro-tasks include Process. nextTick, Promise, MutationObserver, etc

Note that setImmediate and Process. nextTick are unique to Node.

Walk through the Node event loop together

Among these six stages, we should focus on timers, Poll and check, and the related propositions are basically based on them. But before we go to the test point, we still have to go through the whole process of the cycle

  1. Execute global Script code (same as browser);
  2. Emptying the microtask queue: Note that Node emptying the microtask queue is unusual. In the browser, we have only one microtask queue to be processed; However, in Node, there are two types of microtask queues: next-tick queues and other queues. The next-tick queue is specifically used to converge asynchronous tasks dispatched by Process. nextTick. When the queue is cleared, tasks in the next-tick queue are cleared first, and then other micro-tasks are cleared.
  3. Start executing macro-task. Note that Node executes the macro task differently from the browser: in the browser, we queue up and execute one macro task at a time; In Node, we try to clear all tasks in the macro task queue for the current phase (unless the system limit is reached).
  4. Step 3 The system starts to enter 3 -> 2 -> 3 -> 2… (The overall process is shown below)
macro-task-queue ----> timers-queue | | micro-task-queue ----> pending-queue | | micro-task-queue ----> polling-queue | | micro-task-queue ----> check-queue | | micro-task-queue ----> close-queue | | macro-task-queue ----> timers-queue .Copy the code

As a whole, each asynchronous task in Node is executed in batches, “team by team”. The loop format is macro task queue -> micro task queue -> macro task queue -> micro task queue… This alternates.

The event-loop mechanism between Node and browser should not be too difficult for you to understand.

But don’t count your chickens before they hatch. In the event loop section, more common than essay questions are coded reading questions. Let’s go through a series of real questions to reinforce the cognition.

Node event cycle proposition analysis

The Node event loop doesn’t have as many propositions as the browser does, but it can get a lot of people each time

But don’t be afraid, we combined with the above analysis, summed up their own set of Node, browser two sets of event loop mechanism understanding; With the following three hot propositions, you are sure to face event-loop

NextTick and Promise.then

Before we do this, we need to mentally review this feature of a Micro-queue in a Node

Node has a unique way of emptying the microtask queue. In the browser, we have only one microtask queue to be processed; However, in Node, there are two types of microtask queues: next-tick queues and other queues. The next-tick queue is specifically used to converge asynchronous tasks dispatched by Process. nextTick. When the queue is cleared, tasks in the next-tick queue are cleared first, and other micro-tasks are cleared later.

Keep in mind that when you see process.nextTick in a Node asynchronous queue, it’s probably not a good idea

Promise.resolve().then(function() {
  console.log("promise1")
}).then(function() {
  console.log("promise2")}); process.nextTick(() = > {
 console.log('nextTick1')
 process.nextTick(() = > {
   console.log('nextTick2')
   process.nextTick(() = > {
     console.log('nextTick3')
     process.nextTick(() = > {
       console.log('nextTick4')})})})})Copy the code

Q: What is the output of the above code?

Think about it: We now know that whatever microtask you send in that isn’t distributed by Process. nextTick, it’s all queued up for execution behind Process. nextTick. So the output order is

nextTick1
nextTick2
nextTick3
nextTick4
promise1
promise2
Copy the code

The story of setTimeout and setImmediate

SetImmediate is what?

Before we get started, a quick introduction to setImmediate, which can be called in one of two ways

varimmediateID = setImmediate(func, [ param1, param2, ...] );var immediateID = setImmediate(func);
Copy the code

Note that setImmediate, while similar to setTimeout (both of which are used to delay an action), is much more stubborn. SetImmediate does not accept that you specify a time to perform (there is no delay time as an input parameter). SetImmediate recognizes only one time to perform — the check that is closest to it.

SetImmediate does not mediate

The setImmediate() method is used to interrupt a long-running operation and run the callback function immediately after completing other actions, such as events and display updates.

With setImmediate out of the way, let’s take a look at some of the strangest of the Node event loop interview questions

setTimeout(function() {
    console.log('Iron, I was sent by setTimeout.')},0)

setImmediate(function() {
  console.log('Hey, Titty, I'm channelized by setImmediate.')})Copy the code

Q: What is the result of the above code?

Answer: not necessarily!!

Yeah, just not necessarily. I suggest you copy the demo and run it in your Own Node.js file. Do a few more runs and you’ll notice the following amazing patterns

That’s right! As you can see, in this example setImmediate and setTimeout are a mystery — it’s random!

Why is that? (Note that we will do a causal analysis of this phenomenon, which is at least half of the reason, hang in there.)

  • First, a quick tidbit: setTimeout is the second input parameter to the setTimeout function. Its value range is [4, 2^31-1]. In other words, it doesn’t recognize the 0 input. What if I don’t? Force you to drop 4! In other words, the following way
setTimeout(function() {
    console.log('Iron, I was sent by setTimeout.')},0)
Copy the code

It’s equivalent to

setTimeout(function() {
    console.log('Iron, I was sent by setTimeout.')},4)
Copy the code

So this callback is actually delayed by 4ms.

Then you need to realize that the initialization of the event loop takes time.

What do you mean by “it takes time”? This means that the initialization time for the event loop may be greater than or less than 4ms, which leads to two possibilities

  • When the initialization time is less than 4ms, the timers enter the timers stage. However, the setTimeout timer does not run down. Walks to the Check phase and executes the setImmediate callback; The setTimeout callback is executed later in the cycle;
  • When the initialization time is greater than 4ms, the timers enter the timers stage and the setTimeout timer runs out. Therefore, the setTimeout callback is executed directly. After ending the Timers phase, walking and walking led to the Check phase, which logically led to the setImmediate callback.

Combined with our above analysis, I believe you have realized that in fact, each output is correct and conforms to our event cycle principle. The difference in order is caused by the “event loop initialization time” over which we have no control. Therefore, we must answer this question with confidence.

Process the timer in the poll phase

As mentioned earlier, the Poll phase is the key phase where most of the callback tasks are handled. Key stage key stage key stage key stage key stage

As we enter the poll phase, we consider the following two scenarios:

  1. The poll queue is not empty. In this case, it is easy to execute the callbacks one by one and exit the queue until the queue is empty (or reaches the upper limit of the system);
  2. The poll queue is already empty. The event loop does not stay idle: It first checks for setImmediate tasks to be performed, and then proceeds to the Check phase to start processing setImmediate. If there are no setImmediate tasks, then check to see if there are expired setTimeout tasks to handle and jump to the Timers phase.

What if there is no setTimeout task? Then we poll stage also don’t drill this point – no work to do, I wait! At this point, the poll phase enters the wait state, waiting for the callback task to arrive. Poll “pounced” as soon as a pullback entered.

With all this analysis in mind, it is important to note that setImmediate must be used first if the poll phase does both setImmediate and setTimeout. And then setTimeout.

The directory structure is as follows:

  • The test.js file contains arbitrary code
  • In the index.js file, write the following
const fs = require('fs')
const path = require('path')
const filePath = path.join(__dirname, 'test.js')

console.log(filePath)   

// -- read files asynchronously
fs.readFile(filePath,'utf8'.function(err,data){
    setTimeout(function() {
      console.log('Iron, I was sent by setTimeout.')},0)

    setImmediate(function() {
      console.log('Hey, Titty, I'm channelized by setImmediate.')})});Copy the code

Q: What is the output order? Why is that?

Answer: After our previous analysis, the answer to this question is clear: First output console in setImmediate, then output console in setTimeout.

ReadFile is an asynchronous file I/O interface. In Node, I/O callbacks are handled in the poll phase. Therefore, when the readFile callback is executed, this stage is actually reached

In the Poll phase, two tasks are dispatched: a setImmediate callback that is handled in the Check phase and a setTimeout callback that is handled in the Timers phase.

Combining the above figure with our previous analysis, it is obvious that the Check phase is always closer to poll than the Timers phase, and therefore setImmediate is always executed before setTimeout.

So keep this in mind: If setImmediate and setTimeout are both distributed during the callback handled by the poll phase, the sequence is determined — setImmediate must be used before setTimeout.

Attention!! Node11 event loops are converging with browser event loops!

This is a big hole!

An insidious examination question

Let’s look at a problem like this

setTimeout(() = > {
  console.log('timeout1');
}, 0);   


setTimeout(() = > {
  console.log('timeout2');
  Promise.resolve().then(function() {
    console.log('promise1');
  });
}, 0);

setTimeout(() = > {
  console.log('timeout3')},0)
Copy the code

Q: What is the result of executing the above code in the browser and Node? This is a close call, but just remember one thing: Starting with Node11, Node’s event loop has become similar to that of the browser. Notice the “convergence” not the same. One of the most obvious changes

Starting with Node11, tasks assigned by setTimeout, setInterval and other functions in the Timers phase, including setImmediate, are modified to perform the microtask queue as soon as a task in the current phase is completed.

This means that the above problem will have the same result in the browser as it did in Node11 – switch to the higher version and run.

I can show you the results of running the above demo code using Node9.3.0 and Node12.4.1 respectively

The following is the result of the execution in v9.3.0

We see that in the Timers phase, all setTimeout callbacks are executed in sequence, emptying the queue — this is consistent with our previous description of the Node event loop mechanism.

The following is the result in V12.4.1

In the meantime, let’s take a look at the result of the browser running the code above

Obviously, Node11 and above will execute this code with the same results as the browser.

If you encounter a Node event loop related coding problem, you will be able to fill in the previous section on Node11. Let the interviewer know that you’re not just a dork who has memorized dogma and hasn’t updated his knowledge base for 10,000 years. He’s a good programmer who keeps up with the changing times.