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:
- 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
- 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
- … 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:
- How can I do it first
setTimeout
And after the printsetImmediate
- How can I do it first
setImmediate
And 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