“This article is participating in the technical topic essay node.js advanced road, click to see details”
The introduction
Most developers are probably familiar with EventLoop in the browser, and it is important for every front-end developer to master EventLoop.
Of course, the importance of NodeJs to any front-end developer at this stage is undeniable, whether in front-end interviews or daily business. The knowledge of EventLoop is not limited to the execution process in the browser environment.
But how much do you know about event loops in NodeJs? In other words, do you understand the subtle differences between Event Loop and NodeJs in the browser?
The article will cover the following aspects:
-
✨ Concurrency model
-
EventLoop in ✨ browser
-
EventLoop in ✨ NodeJs
-
✨ browser and EventLoop in NodeJs
In this article, you will explore the EventLoop mechanism in different operating environments from the above four aspects to truly master EventLoop.
Concurrency model
The most common word we hear in JavaScript is probably the so-called “single thread”, which leads to the so-called asynchronous parallel model in JS that is different from many background languages.
In short, although JavaScript performs all operations in a single thread, it implements so-called “asynchrony” based on the process of an EventLoop, giving us a multithreaded feel.
We’re going to talk a little bit about some simple concepts before we get into it.
The stack
For example, our daily function execution is essentially stack based operation. There is a call stack in JS that keeps track of all the operations that need to be performed.
Whenever a function completes, it pops up from the top of the stack. Take this code for example:
const bar = () = > console.log('bar')
const baz = () = > console.log('baz')
const foo = () = > {
console.log('foo')
bar()
baz()
}
foo()
Copy the code
The stack execution process is shown in the figure below:
The image is from the official NodeJs documentation.
In fact, this is a very simple process, the code function to perform a first-in, first-out process based on the stack.
The call stack is responsible for keeping track of all operations to be performed. Every time a function completes, it pops off the top of the stack.
The heap
Objects are allocated in a heap, a computer term used to denote a large (usually unstructured) area of memory.
The concept of a heap isn’t really important to our understanding of EventLoop, so we just need to understand that in JavaScript for all variables of reference type, we actually store them in the heap.
The stack stores only Pointers to the heap. The basics of stacks and heaps are pretty well understood, so I won’t go over them too much here.
The event queue
We talked about how executing our code in Javascript is essentially a stack, but how execution tasks (such as functions above) are pushed onto the stack.
Here we have to introduce the concept of an Event Queue, which is responsible for sending the functions to be executed on the stack for processing. It ensures that all the functions are sent in the correct order according to the data structure of the Queue.
In simple terms, when we complete the stack we mentioned above. At this point, the JavaScript will continue to enter the Event Queue to see if there are any tasks that need to be executed, and if there are, it will continue to execute the tasks in the Queue. Note that the order of execution in the event queue is based on queue first-in, first-out order.
It is important to note that the execution process in JS is based on the “execute to finish” process. That is, every time there is a new message that is fully executed, other messages will be executed in the JS thread.
For example, let’s say my page is executing a piece of logic. At this point the user clicks the button that owns the bound event. The event will be queued immediately, but it will not be executed immediately.
It still needs to wait for all queued tasks to complete before it can be executed.
In series process
We talked briefly about stacks and event queues. Here’s a simple example using SetTimeout in the browser to help you understand this process:
function fn() {
const a = '19Qingfeng'
console.log(a)
setTimeout(() = > {
console.log('hello')},0)
console.log(a+1)
}
fn()
Copy the code
For example, during our code execution, fn() execution will be pushed onto the stack. JS calls this function on the stack, and FN first executes line by line.
When a setTimeout operation is processed in the stack (FN), it is sent to the appropriate timer thread for processing, and the timer thread waits until the specified time is satisfied to send the operation back to processing.
Note that the timer thread will send the callback function to the event queue, and the event loop will check the current stack to see if the function is running. If empty, a new function is added from the event queue and pushed onto the stack for execution. If not, the function call in the current stack continues to be processed.
While Javascript itself is single-threaded, we can make use of browser-related threads and event queues to make it asynchronous.
Therefore, we have achieved the so-called “asynchronous” effect based on the event cycle model.
EventLoop in the browser
There are plenty of good articles on the so-called EventLoop in browsers, and I’m sure you’re all familiar with the topic of EventLoop in browsers, so I won’t spend too much time on it here.
The picture is from Xiuyan’s booklet “Principles and Practices of Front-end Performance Optimization”.
In fact, this picture of EventLoop in the browser is pretty much everything.
The main need is in the browser, the so-called macro-task stands for: setTimerout, setInterval, script scripts, UI rendering, etc.
Micor-task stands for: Promise, MutationObserver, etc.
We can see this clearly in the figure, for example, when we execute a Macro-task (such as executing a script script that has already been loaded).
-
When a script script (Macro) is encountered during the first page execution, it is pushed onto the stack to execute the corresponding logic synchronously.
-
When a so-called Macro-task is encountered during execution, it is handed off to the corresponding thread for processing.
-
When a so-called micro-task is encountered during execution, it is also handed off to other threads.
-
When the Macro-task/micro-task in the above code reaches execution time while the script is still executing, their callback handlers are pushed into their respective event queues.
It is important to note that being pushed into the event queue does not mean that it is immediately added to the stack for execution.
-
When the script script is finished, it means that the stack has been emptied. At this point, the event loop checks that the call stack is empty.
-
At this point, tasks in Micaro-Tasks are successively pushed onto the call stack for execution (first-in, first-out), clearing all tasks in the Micro queue.
-
Next, as shown in the figure. The UI thread performs page rendering after clearing the Micaro-Tasks task queue. (In other words, every EventLoop doesn’t necessarily have to be accompanied by a page rendering.)
-
After that, tasks in the related Worker will be processed.
In fact, this is a complete browser related EventLoop processing, it is very simple. The only thing to keep in mind is that every time the EventLoop takes out a Macro to execute and after the macro is executed, all micAO tasks in the queue are cleared.
Of course, after the Worker logic is processed, the macro at the top of the queue will be taken from macro-Tasks to repeat the process.
Of course, there is a little tip here where we can see that all the micro tasks are cleared before each page rendering. This means that our manipulation of the Dom in a microtask would have caused the UI thread to do less drawing (and faster display to the user).
The preferred solution of nextTick asynchronous update principle in Vue is Promise, which is actually designed based on this behavior. Interested friends can check it privately.
The Node of the EventLoop
We briefly described the execution of an EventLoop in the browser, but now we are going to take a look at how the so-called EventLoop is executed in NodeJs.
Node APi
This is how the event loop is described in the Official NodeJs guide, but before diving into this diagram let’s take a look at what API tasks NodeJs provides for the browser environment.
- Process.nextTick
The process. nextTick method is a very important API in the NodeJs EventLoop. Recall that in the browser’s time loop the EventLoop clears all micaro callbacks generated under the current macro.
The so-called execution timing of process. nextTick is that after the synchronization task is completed, the micro-task is about to be pushed into the stack before being pushed into the stack for execution.
In other words, process.nexttick is simply logic that is executed as soon as the current call stack is cleared, and you can actually think of it as a micro (although it is not officially considered part of EventLoop).
It takes precedence over any micro in the EventLoop.
- setImmediate
SetImmediate is also an API in NodeJs. It means to make the setImmediate() function when executing some code asynchronously (but as quickly as possible).
Similar to setTimeout(()=> {},0), setImmediate is also a Macro macro task. We will discuss the timing of this and setTimeout in more detail later.
- I/O operations
We all know that NodeJs is another Runtime in which JavaScript is executed without browser V8, which means that NodeJs can be used for I/O operations (such as reading from the network, accessing a database or file system).
For I/O operations, you can think of the callback it generates as a macro macro task queue.
Of course, the FileReader API is also available on the current Web to read files.
Event Loop
Now let’s go back and look at this picture:
Let’s start with a brief description of what each layer in the figure represents. Each layer you can think of as a queue:
- Stage of timers.
Scheduling callbacks that have been setTimeout() and setInterval() are executed during the Timers phase.
- Pending Callbacks phase.
In the last loop queue, those that have not finished executing are executed at this stage. For example, I/O operations deferred to the next Loop.
- idle, prepare
We don’t need too much relationship for this step, it’s just called inside NodeJs. We cannot operate this step, so we only need to know that idle prepare exists.
- poll
This phase, called the polling phase, detects new I/ O-related callbacks, but it is important to note that this phase is blocked (meaning that subsequent phases may not be executed).
- check
The Check phase detects that the setImmediate() callback is executed during this phase.
- close callbacks
This phase executes a series of closed callback functions, such as socket.on(‘close’,…). .
In fact, the event loop mechanism in NodeJs is mainly based on the above stages, but for us only the timers, Poll and Check stages are important, because these three stages affect the execution order of our code writing.
Pending callbacks, idle, prepare, and close callbacks are not strongly coupled to the execution order of our code, and sometimes we don’t even care about them at all.
Let’s explore the EventLoop process described in NodeJs above with a few questions:
process.nextTick
Let’s start with this code:
function tick() {
console.log('tick');
}
function timer() {
console.log('timer');
}
setTimeout(() = > {
timer();
}, 0);
process.nextTick(() = > {
tick();
});
// log: tick timer
Copy the code
As we saw in the previous step, all nextTick is cleared immediately after the current call stack is cleared before entering the so-called EventLoop.
That’s when we can use this simple picture
The above code is called quite simply. When the code encounters process.nextTick and timer in sequence, it pushes them to their respective queues.
When the code in the stack is executed, it will check the existence of the corresponding tick function in nextTick first, and then it will take out the tick and enter the stack for execution.
When tasks in nextTick are empty, the timers stage is called and all generated timers are also empty, that is, the timer function is executed.
Careful friends may notice that process.nextTick is not placed in the EventLoop. This is also explicitly stated in NodeJs:
While process.Nexttick is part of the asynchronous API, it is not technically part of the event loop.
The execution order of process.nextTick will be covered in detail in a bit of microtask, although it is not part of EventLoop.
But we can definitely think of it as micro. The two are completely equivalent in execution, and we can simply think of process.Nexttick as the micro (microtask) with the highest priority.
SetTimerout & setImmediate
I believe the above code is not difficult for you, and then let’s look at this code:
function timer() {
console.log('timer');
}
function immediate() {
console.log('immediate');
}
setTimeout(() = > {
timer();
}, 0);
setImmediate(() = > {
immediate();
});
Copy the code
Let’s try to analyze this code before publishing the results. First, we said that when the script finishes executing, this code pushes the corresponding timer function and immediate function into the queue of the timer and check phases, respectively.
As we understand it, after the synchronization script is executed:
-
It first checks for the presence of process.nextTick and shows that there are no Nexttick-related calls in the code. So I’m going to skip it.
-
Then we will enter the EventLoop phase, which is also called timer phase, because we have the timer function setTimerout(timer,0) in our code.
Therefore, when the Loop reaches the timer phase, it should be pushed into the corresponding Timers queue because the timer meets the time. When the EventLoop reaches the Timer phase, the timer’s callback is pulled out to execute it.
So it’s not hard to imagine the console executing this function to print the timer.
-
Pending Callbacks are then entered, showing that the last EventLoop did not have any operations that reached the upper limit. So it’s empty.
-
The system enters idle prepare.
-
So notice that now we’re going to go to the poll phase, and the poll phase doesn’t have any IO related callbacks, return and in the poll phase he’s going to detect setImmediate in our code, And setImmediate’s callback is already pushed into the check phase.
-
So, there is no so-called blocking effect at poll. The check phase calls the setImmediate callback immediate in the code, so the console outputs immediate.
-
Second, after the check phase clears the immediate, the final close callbacks in the Loop are entered.
It shows that everything looks fine according to our analysis, and the console should print timer first and immediate second.
Let’s see if it works as we expected.
That’s what we expect, right? But if you run this code multiple times you’ll notice a difference. (It’s even possible that your results are now different from mine.)
When I run the same code here, something strange happens.
The same piece of code has a completely different execution result. This time, the so-called immediate is executed before the output of the timer.
Any seemingly irregular results are actually hidden behind the same logic.
First of all, trust me. According to the EventLoop execution results we analyzed earlier, there is no problem.
Immediate execution first
The reason for this output is that setTimeout is at work.
In NodeJS, setTimeout(cb,0) actually has a minimum execution time of 1 ms, which is used as setTimeout(cb,1), which you can see at ➡️.
When
delay
is larger than2147483647
or less than1
, thedelay
will be set to1
. Non-integer delays are truncated to an integer.
Some of you may remember that the minimum interval of setTimeout is 4ms, and 4ms is also the minimum interval of setTimeout. However, this is the minimum interval in the browser, not Node.
At this point, I believe that some students have already realized why the execution result is random with the timer and immedate appearing randomly.
Precisely because setTimeout has a minimum indirection of 1ms, if our computers are good enough.
Therefore, after the above synchronization code is executed and the EventLoop is entered, all these occur within 1ms. Obviously, the timers stage does not reach the corresponding time due to setTimeout in the code. In other words, its corresponding callback is not pushed into the current timer.
Naturally, the function named timer is not executed either. It goes on to the next phase. Loop checks down in turn.
When the poll phase is executed, even though the corresponding timer function has been pushed into timers. The Poll phase detects the presence of setImmediate, so the Device continues into the Check phase and does not revert to Timers.
Therefore, these two apis are invoked in the main call stack based on Timeout and code execution time, resulting in immediate output and timers being executed later.
SetTimeout to perform
At this point, it becomes very simple to go back and analyze the scenario where setTimeout is executed first.
If your computer is not performing well, it takes more than 1ms for the code in the stack to enter the timers phase when the setTimeout timer is executed and the EventLoop enters the corresponding timers phase.
At this point, there is no doubt that it will be pushed into the corresponding Timers because the timer time has been satisfied during the timers phase.
Of course, the execution order is reversed by printing timer first and then immediate.
If you don’t believe me, try executing the following code 100 times to see what happens:
function timer() {
console.log('timer');
}
function immediate() {
console.log('immediate');
}
process.nextTick(() = > {
for (let i = 0; i++; i < 3000) {
// do nothing}})setTimeout(() = > {
timer();
}, 0);
setImmediate(() = > {
immediate();
});
Copy the code
It must print timer first and immediate next.
How to ensure that setImmediate is faster than setTimeout
After learning about EventLoop in Node, it’s pretty easy to make sure that setImmediate is faster than setTimeout.
The reason for this question is that I have been asked it myself in some interviews.
Recall for a moment the initial EventLoop diagram, if we want to ensure that setImmediate executes faster than setTimeout, which is equivalent to ensuring that both are called after the Timers phase in EventLoop.
Then setImmediate will be faster than setTimeout anyway, such as:
const fs = require('fs');
const path = require('path');
// The fs.readfile callback is executed at the end of the IO in the poll phase
// If setImmediate does exist in the IO callback, the next phase of the EventLoop must go to the Check phase
// Then setImmediate must take precedence over the setImmediate callback
fs.readFile(path.resolve(__dirname, 'package.json'), (err) = > {
if (err) {
console.log(err, 'err');
}
setTimeout(() = > {
console.log('timer');
});
setImmediate(() = > {
console.log('immediate');
});
});
Copy the code
Micro Micro tasks
Of course, the six steps in EventLoop described above are all relative to Macro tasks.
Two essential concepts in EventLoop: Macro and Micro.
Let’s take a look at how microtasks are performed in NodeJs EventLoop:
SetImmediate (() => {console.log('immediate start ') promise.resolve ().then(console.log('immediate' + 1)); Promise.resolve().then(console.log('immediate' + 2)); The console. The log (' immediate end '); }); SetTimeout (() => {console.log('timer start '); Promise.resolve().then(console.log('timer' + 1)); Promise.resolve().then(console.log('timer' + 2)); The console. The log (' end of the timer); }, 0);Copy the code
The reason for the above code regarding Immediate and Timeout is that we’ve talked about them before, and it’s not the focus of our discussion here.
You can see that either the immediate or timer task is executed first, and the Micro task generated in the queue is executed before the next Macro is executed.
In essence, NodeJs is similar to the browser. Although there are multiple execution queues under NodeJs, the logic of each execution is the same: after the execution of a macro task, all the microtasks generated in the current queue are immediately emptied.
Of course, under NodeJs < 10.0, it will empty one queue before empting all micros in the current queue.
setImmediate(() = > {
console.log('immediate1 start')
Promise.resolve().then(() = > console.log('immediate' + 1.'Microtask execution'));
Promise.resolve().then(() = > console.log('immediate' + 2.'Microtask execution'));
console.log('immediate1 end');
});
setImmediate(() = > {
console.log('immediate2 start');
Promise.resolve().then(() = > console.log('immediate' + 3.'Microtask execution'));
Promise.resolve().then(() = > console.log('immediate' + 4.'Microtask execution'));
console.log('immediate2 end');
});
/* LOG IMMEDIate1 Starting IMMEDIate1 Ending IMMEDIate1 Microtask execution IMMEDIate2 Microtask execution IMMEDIate2 Starting IMMEDIate2 Ending IMMEDIate3 Microtask execution Immediate4 Micro task execution */
Copy the code
After understanding EventLoop in browser, I believe it will be very simple for everyone to implement Micro and Macro in Node.
Note that while NodeJs does not officially treat Proess.nexttick as a micro, for Proess.nexttick you can think of it as a micro except that it has the highest priority of all other micro in the current queue.
In series process
Let’s briefly concatenate the EventLoop in NodeJS.
Micro
Press. NextTick and all previous microtasks are processed first after the main call stack ends:
As shown in the figure above, we can simply call process. nextTick and Micro together as micro.
timers
Timers callback is the first stage of the EventLoop event queue.
We can see that when the Timers phase is entered, timers are checked to see if there are timer tasks that meet the conditions. If it exists, the corresponding timer (callback generated by timer) will be successively removed and pushed into the stack (JS execution stack) for execution.
The prcess.nexttick -> micro step is still performed at the end of each execution to clear the next timer task.
poll
After emptying all timers in the queue, the Loop enters the poll phase, which first checks for the existence of callback corresponding to I/O.
If there is an I/ O-related callback, then the corresponding JS call stack will be pushed to execute, and the corresponding process. nextTick and micro will be cleared after each task is executed.
Of course, if the second phase generates a timer, it will not execute in this Loop because the EventLoop has already reached the poll phase.
It in turn pulls out the relevant I/O callbacks and pushes them onto the stack to clear them.
It is important to note that during the poll polling phase, the following happens:
-
If the polling queue is not empty, the event loop loops through the callback queue and synchronously executes them until the queue is exhausted or a system-specific hard limit is reached.
-
If the polling queue is empty, two more things happen:
- If the script is
setImmediate()
Schedule, the event loop will endPoll (polling)Phase, and continueCheck (check)Phase to execute the scripts that are scheduled. - If the scriptHas not been
setImmediate()
The event loop waits for the callback to be added to the queue and then executes immediately.
- If the script is
Notice in the diagram that we start the Loop after the Timer phase.
In fact, at this point, the meaning of each step in the flow chart of EventLoop at the beginning of this article has been explained with confidence. I’m sure this shouldn’t be a problem for you.
There are still additional pending callbacks and idle. Prepar is rarely used in our daily application, or we simply cannot control the execution sequence here through API, so we did not discuss them in depth.
Node & Browser
After understanding the execution mechanism of EventLoop in different environments, we can find that the execution mechanism of EventLoop in browser and Node is essentially the same, which is to empty the micro tasks in the queue after executing a macro task.
The only difference is that NodeJs implements some additional custom queues for EventLoop, which is based on the event mechanism implemented in Libuv itself.
Of course, there is also the essential process.nexttick, which is not strictly micro, but can be understood as having the highest priority micro.
At the end
At the end of this article, thank you to everyone who can see here.
I hope the content of the article can help more front-end students grow up!