This fall, I was asked a question in meituan: Have you ever heard of Event Loop?

At that time, I was a face of meng, because never heard of this professional term. But the interviewer was very friendly, he said that’s ok, then you can do a question, see the following code implementation result?

console.log('1')

setTimeout(function callback(){
	console.log('2')},1000)

new Promise((resolve, reject) = > {
    console.log('3')
    resolve()
})
.then(res= > {
    console.log('4');
})

console.log('5')
Copy the code

Obviously, this question is to test your understanding of Event Loop. As expected, I did not get this question right at that time, but I silently wrote it down and came back to sort out the knowledge after studying

Do you know what the correct answer is? Here is a foreshadowed, you can do this problem yourself, the answer will be published in the article

  • Public account: front-end impression
  • Sometimes there are book sending activities, remember to pay attention ~
  • [Interview questions], [front-end must-see e-book], [Data structure and algorithm complete code], [front-end technology exchange group]

How does JavaScript work

When you first started learning JavaScript, you probably heard the phrase: JavaScript is single-threaded

What is a single thread? It is a lot of JS code, which is executed from the top to the next line of code, that is, only after the previous line of code is executed the next line of code

This is also to ensure that our code logic is sequential when we implement certain functions

At this point, someone would ask a question, and they would send me a piece of code that looked like this

console.log('1')

setTimeout(function (){
	console.log('2')},1000)

console.log('3')

/* Run result: 1 3 2 */
Copy the code

JS is single threaded, line by line of code. Why does this code print 3 first and then 2?

Gives a knowledge point, some in the JS code is executed asynchronously, the so-called asynchronous, is not blocked the run of the code, and will also open a space to perform the asynchronous code, the rest of the synchronization code is still normal execution, if there are other code in the asynchronous code, will at some point after the asynchronous code in other code execution. For example, the setTimeout function in the above code is asynchronous, and there is an internal synchronization code console.log(‘2’)

The moment mentioned here is what we will focus on later in this article, but I won’t go into it too much here

So where does the extra space for asynchronous execution come from? That is, of course, provided by the JS runtime environment, and the two main JS runtime environments are: browser and Node, we will also explain the JS runtime mechanism based on these two runtime environments

JavaScript in the browser

The reason why JS can run in a browser is because browsers provide a JavaScript engine by default, which provides a runtime environment for JS

Here is a simplified view of a JavaScript engine:

On the left is the memory heap, which is the memory the browser uses to allocate code to run. On the right side of the figure is the call stack. Whenever a piece of code is run, the JS code will be pushed into the call stack, and then out of the stack after the execution

We’re not going to talk too much about the memory heap, but we’re going to talk about the call stack

(1) Call stack

What is a call stack? Here is a piece of code to analyze the running of the call stack

function multiply(a, b) {
	return a * b
}

function calculate(n) {
	return multiply(n, n)
}

function print(n) {
	let res = calculate(n)
	console.log(res)
}

print(5)
Copy the code

When this code runs in the browser, it first queries the three defined functions multiply, calculate, and print; Then print(5) is executed. Since these three functions are called, calculate and multiply are called in turn

Now, let’s look at what happens inside the call stack as this code executes

Here is another way to verify the existence of the call stack and its contents. Let’s write a code like this:

function fn() {
    throw new Error('isErr')}function foo() {
    fn()
}

function main() {
    foo()
}

main()
Copy the code

Then run it in a browser and you get the following:

When an error is thrown while the code is running, the browser prints out the entire call stack, as we would expect. The call stack looks like this:

The above process involves synchronous code, so how to open up a new space for asynchronous code to run as we mentioned above?

This is where the concept of an Event Loop comes in

(2) Event Loop

An Event Loop translates as an Event Loop, so what is the Event Loop? Here we show the complete browser event loop diagram, take a look

All kinds of things in the browserWeb APIAsynchronous code is provided with a separate space to run, and when the asynchronous code is finished, the callback from the code is sent toTask Queue(task queue), when the call stack is empty, then push the callback function in the queue into the call stack for execution. When the stack is empty and the task queue is also empty, the call stack will continue to detect whether there is code in the task queue to execute, and this process is a complete Event Loop

We can use a simple example to get a feel for the event loop

console.log('1')

setTimeout(function callback(){
	console.log('2')},1000)

console.log('3')
Copy the code

Let’s see what’s going on with a GIF

(3) Macro tasks and microtasks

After a simple understanding of the process of Event Loop, let’s look at another question to see if we can answer correctly

console.log('1')

setTimeout(function callback(){
	console.log('2')},1000)

new Promise((resolve, reject) = > {
    console.log('3')
    resolve()
})
.then(res= > {
    console.log('4');
})

console.log('5')
// What is the order in which this code is printed?
Copy the code

Here are the answers

// Correct answer:
			1
			3
			5
			4
			2
Copy the code

Why promise and setTimeout are both asynchronous? Why does the former take precedence over the latter?

This brings us to two other concepts, macroTask and microtask.

The following is a list of common macro and microtasks in our browser

The name of the Examples (commonly used)
Macro task SetTimeout, setInterval, UI Rendering
Micro tasks Promise, requestAnimationFrame

In addition, when both macro tasks and microtasks are in the Task Queue, the priority of the microtask is greater than that of the macro Task, that is, the microtask is executed before the macro Task is executed

Therefore, the above code prints 4 first and then 2

Of course, since macro tasks and micro tasks are distinguished, there are two kinds of queues to store them, namely Macro Task Queue and Micro Task Queue, as shown in the figure

According to relevant regulations, when the call stack is empty, the detection steps for these two queues are as follows:

  1. Check whether the microqueue is empty. If not, take out a microtask and push it into the stack. Then perform Step 1. If no, go to Step 2
  2. Check whether the macro queue is empty. If not, fetch a macro task and push it to the stack. Then perform Step 1. If no, go to Step 1
  3. … Reciprocating cycle

So let’s take a look at how that code calls

So after you’ve seen this, let’s go back to the one above and see if we got it right.

JavaScript in Node.js

Note: This discussion is all about node.js 11.x and above

This article discusses JS in the browser environment and node.js environment respectively. There is a difference between the latter and the former, and the latter process is more detailed

(1) Event Loop in node

Let’s look at a simple diagram of node.js’ Event Loop

Node.js’ Event Loop is based on Libuv

According to the official documentation of Node.js, the sequence of the event loop is divided into six phases, each of which handles a specific task:

  • Timers: A timer phase that handles setTimeout and setInterval callbacks
  • Pending Callbacks: Callbacks used to perform certain system operations, such as TCP errors
  • Idle, prepare: Used within the Node. No further information is required
  • Poll: In the polling phase, the I/O queue is executed and the timer is checked
  • Check: Perform a callback to setImmediate
  • Close callbacks: handles closed callbacks, such as socket.destroy()

There are only four phases to focus on: Timers, polls, checks, and close Callbacks

Each of these four phases has its own macro queue, and only after the tasks in the macro queue of this phase have been processed can the next phase be entered. In the process of execution, the micro queue is constantly detected to see if there is a task to be executed. If there is, the task in the micro queue is executed. When the micro queue is empty, the task in the macro queue is executed. Instead, all macro tasks in the current phase of the queue are run before the microqueue is detected. For versions after 11.x, although I have not found the relevant text on the official website is like this, but through countless times of running, let’s say it is like this, if you find the relevant instructions, you can leave a comment)

In the same way, Node.js has macro tasks and microtasks. Let’s take a look at some of the most common ones

The name of the Examples (commonly used)
Macro task SetTimeout, setInterval, and setImmediate
Micro tasks Promise, process. NextTick

As you can see, node.js adds two tasks to the browser, namely the macrotask setImmediate and the microtask Process.nexttick

SetImmediate is processed in the check phase

Process. nextTick is a special microtask in Node.js, so it is given a separate queue, called the Next Tick Queue, with a higher priority than the other microtasks. The former is executed first

In summary, Node.js involves four macro queues and two micro queues in the event loop, as shown in the figure below

Now that we know the basic process, let’s write a simple problem

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

setImmediate(() = > {
    console.log(2);
})

new Promise(resolve= > {
    console.log(3);
    resolve()
    console.log(4);
})
.then(() = > {
    console.log(5);
})

console.log(6);

process.nextTick(() = > {
    console.log(7);
})

console.log(8);

/* Print result: 3 4 6 8 7 5 1 2 */
Copy the code

First of all, of course, the synchronized code is printed first, so 3, 4, 6 and 8 are printed first

To check for asynchronous code, setTimeout is fed into the Timers queue. SetImmediate is sent to check queue; Then () is sent to the other MicroTask Queue; Process. NextTick Is entered into the Next Tick Queue

The next tick queue has a higher priority than the other Microtask queue, so the next tick queue is printed with a 7 and then a 5. At this point, the tasks in the microqueue are all executed, and then the phase in the timers queue is displayed. Therefore, 1 is printed and the queue in the current phase is empty. The poll phase is entered in sequence, but the queue is empty, so the check phase is entered. I said that this phase is designed to mediate, and that in the end 2 is printed

2) setTimeout and setImmediate

In a loop, the timers phase precedes the check phase, so that means that setTimeout must do it before setImmediate does. Let’s look at an example

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

setImmediate(() = > {
    console.log('setImmediate');
})
Copy the code

We run this code several times with Node and get the following two results:

// The first result
setTimeout
setImmediate

// The second result
setImmediate
setTimeout
Copy the code

Why is that?

Here we set the delay time for setTimeout to be 0. It looks like there is no delay, but it actually runs with a delay time greater than 0

Then it takes time for node to start an event loop. Assuming that it takes 2 milliseconds for node to start the event loop and the actual delay for setTimeout to run is 10 milliseconds, that is, the event loop starts earlier than setTimeout, then there is no callback for setTimeout to execute when the first round of the event loop runs to timers. So we go to the next phase, and even though the delay time for setTimeout is up, it can only be executed on the next loop, so the event loop prints setImmediate first, and then prints setTimeout on the next loop.

That’s where the second result comes in

It would be easier to understand why the actual delayed event of setTimeout is smaller than the open event of the Node event loop, so it can be executed in the first round of the loop

With the above reasons understood, here are two questions:

  1. How can I do it firstsetTimeoutAnd after the printsetImmediate
  2. How can I do it firstsetImmediateAnd after the printsetTimeout

Let’s implement these two requirements separately

Implement a:

Since we want the setTimeout to be printed first, we want it to be executed on the first round of the loop, so we just need to start the event loop later. So you can write a synchronized piece of code that makes the synchronized execution event a little longer and then ensure that the setTimeout callback has been fed into the Timers queue when the timers phase enters

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

setImmediate(() = > {
    console.log('setImmediate');
})

let start = Date.now()
// Let the synchronized code run for 30 milliseconds
while(Date.now() - start < 30)
Copy the code

Running the code several times, I find that Every time, setTimeout is printed first, and then setImmediate prints

To realize two:

Since we want the setTimeout to be printed after the second round of the loop, we can have the setTimeout run after the first round of the event loop has skipped the Timers phase

In the beginning, we said that the poll phase is designed to handle various I/O events, such as file reading, so we can put setTimeout and setImmediate code in a callback to a file reading operation, When the first loop reaches the poll phase, the setTimeout is sent to the Timers queue, but the timers have already been skipped, so it will only be printed in the next loop. At the same time, setImmediate is sent to the check queue, so that after leaving the poll phase, setImmediate can print it first

const fs = require('fs');

fs.readFile(__filename, () = > {
  setTimeout(() = > {
    console.log('setTimeout');
  }, 0);
  setImmediate(() = > {
    console.log('setImmediate');
  });
});
Copy the code

Running the code several times, I find that every time I print setImmediate first, then setTimeout

Iv. Conclusion

This is the end of a complete Event Loop, the author also spent two days to understand it, and put it into a blog, I hope this article can be helpful to you, haha the most important thing is, do not like the author in the interview again on this stumble

I’m Lpyexplore, a Python crawler into the front end explorer, your likes and collections are my biggest motivation

Finally, you can follow my wechat public account: Front impression, share front face, cutting-edge technology and the welfare of fans from time to time