This article brings you a detailed explanation of the event loop workflow and life cycle of Node.js. It has certain reference value. Friends in need can refer to it. I hope it will be helpful to you.
This article will explain in detail the node.js event loop workflow and life cycle
One of the most common misunderstandings is that the event loop is part of the Javascript engine (V8, spiderMonkey, etc.). In fact, the event loop mainly uses the Javascript engine to execute code.
First of all, there is no stack, and secondly, this process is complicated, with multiple queues (like queues in the data structure) involved. But most developers know how many callback functions are pushed into a single queue, which is completely wrong.
Due to the wrong node.js event loop diagram, some of us thought u had two threads. One executes Javascript and the other executes the event loop. In fact, they all run in one thread.
Another very big misunderstanding is that the callback function of setTimeout is called after the given delay is completed (maybe OS or kernel) advances a queue.
As a common event loop description has only one queue; so some developers think that setImmediate puts the callback in the work queue the front. This is completely wrong. The work queues in Javascript are first-in, first-out
When we begin to describe the event loop workflow, It is very important to know its architecture. The following figure shows the real workflow of the event loop:
Different boxes in the figure represent different stages, and each stage performs specific work. Each stage has a queue (it is said to be a queue here mainly for better understanding; the real data structure may not be a queue), and Javascript can be executed in any stage (except idle & prepare). You can also see nextTickQueue and microTaskQueue in the picture. They are not part of the loop and the callbacks in them can be executed at any stage. They have higher priority to execute.
Now you know that the event loop is a combination of different stages and different queues; below is a description of each stage.
This is the stage at the beginning of the event loop. The queue bound to this stage retains the timer (setTimeout, setInterval) The callback, although it does not push the callback into the queue, uses a minimal heap to maintain the timer and execute the callback after the specified event is reached.
This phase executes the callbacks in the pending_queue in the event loop. These callbacks are pushed by previous operations. . For example when you try to write something to tcp, the job is completed and the callback is pushed into the queue. The callbacks for error handling are also here.
Despite the name, it runs every tick. Prepare also runs before the polling phase begins. Anyway, these two stages are the stages where node mainly does some internal operations.
Perhaps the most important phase of the entire event loop is the poll phase. This phase accepts new incoming connections (new Socket establishment, etc.) and data (file reading, etc.). We can break the polling phase into several different parts.
If there are things in the watch_queue (the queue that is bound to the polling phase), they will be executed one after another until the queue is empty or the system reaches the maximum limit.
Once the queue is empty, node will wait for new connections. The wait or sleep event depends on a variety of factors.
The next phase of polling is check pahse, which is dedicated to setImmediate. Why do you need a dedicated queue to handle setImmediate callbacks? This is due to the behavior of the polling phase, which will be discussed later in the process section. For now just remember that the check phase mainly handles the setImmediate() callback.
The closing of the callback (stocket.on('close', () => {})) is all handled here, More like a cleanup phase.
The task in nextTickQueue is retained in the callback triggered by process.nextTick(). microTaskQueue retains callbacks triggered by Promise. None of them are part of the event loop (not developed in libUV), but in node. When C/C++ and Javascript intersect, they are called as quickly as possible. Therefore they should be executed after the current operation is run (not necessarily after the current js callback is executed).
When running node my-script.js in your console, node sets up the event loop and then runs your main module (my- script.js) Outside of the event loop. Once the main module has executed, node will check if the loop is still alive (is there anything left to do in the event loop)? If not, it will exit after executing the exit callback. process, on('exit', foo) callback (exit callback). But if the loop is still alive, node will enter the loop from the timer phase.
The event loop enters the timer phase and Check if there is anything in the timer queue that needs to be executed. Okay, this sounds very simple, but the event loop actually has to perform a few steps to find the appropriate callback. Actually timer scripts are stored in heap memory in ascending order. It first obtains an execution timer and calculates whether now-registeredTime == delta? If so, it will execute the timer's callback and check the next timer. Until it finds a timer that has not yet been scheduled, it will stop checking other timers (because the timers are all arranged in ascending order) and move to the next stage.
Suppose you call setTimeout 4 times to create 4 timers, with differences of 100, 200, 300, and 400 relative to time t.
Assume that the event loop enters the timer phase at t 250. It will first look at timer A, whose expiration time is t 100. But now the time is t 250. So it will execute the callback bound on timer A. Then check timer B and find that its expiration time is t 200, so B's callback will also be executed. Now it will check C and find that its expiration time is t 300, so it will leave it. The time loop will not check D because the timer is set in ascending order; therefore D has a larger threshold than C. However, this phase has a system-dependent hard limit, and if the maximum number of system dependencies is reached, it will move to the next phase even if there are unexecuted timers.
After the timer phase, the event loop will enter the pending I/O phase, and then check Check whether there are callbacks from previous pending tasks in the pending_queue. If there is, execute them one after another until the queue is empty or the maximum limit of the system is reached. After that, the event loop will move to the idle handler phase, followed by the preparation phase to do some internal operations. Then it may finally enter the most important phase, the poll phase.
As the name says, this is an observation phase. Observe whether there are new requests or connections coming in. When the event loop enters the polling phase, it will execute scripts in watcher_queue, including file read responses, new socket or http connection requests, until events are exhausted or the system dependency limit is reached like other phases. Assuming there are no callbacks to execute, the poll will wait for a while under certain conditions. If there are any tasks waiting in the check queue, pending queue, or closing callbacks queue or idle handler queue, it will wait 0 milliseconds. It then determines the wait time to execute the first timer (if available) based on the timer heap. If the first timer threshold is passed, there is no need to wait (the first timer will be executed).
After the polling phase ends, the check phase comes immediately. There are callbacks triggered by api setImmediate in the queue at this stage. It will be executed one after another like other stages until the queue is empty or the maximum limit of the dependent system is reached.
After completing the tasks in the check phase, the next destination of the event loop is to handle the close callback of the close or destruction type. After the event loop finishes executing the callbacks in the queue at this stage, it checks whether the loop is still alive, and if not, exits. But if there is still work to do, it goes to the next loop; hence the timer phase. If you consider the timers (A & B) in the previous example to have expired, now the timer phase will start from timer C to check for expiration.
So, when do the callback functions of these two queues run? They of course run as fast as possible before moving from the current stage to the next stage. Unlike the other stages, they do not have system-dependent hangover restrictions, and node runs them until both queues are empty. However, nextTickQueue will have a higher task priority than microTaskQueue.
A common word I heard from Javascript developers is ThreadPool. A common misconception is that nodejs has a process pool that handles all asynchronous operations. But in fact the process pool is in the libUV (a third-party library used by nodejs to handle asynchronous) library. The reason why it is not shown in the diagram is because it is not part of the loop mechanism. Currently, not every asynchronous task will be processed by the process pool. libUV makes flexible use of the operating system's asynchronous APIs to keep the environment event-driven. However, the operating system's API cannot do file reading, dns query, etc. These are handled by the process pool, which has only 4 processes by default. You can increase the number of processes up to 128 by setting the uv_threadpool_size environment variable.
Hope you understand how the event loop works. Synchronous while in C language helps Javascript become asynchronous. It only handles one thing at a time but is very blocking. Of course, wherever we describe theory, it's best understood with examples, so let's understand this script through some code snippets.
setTimeout(() => {console.log('setTimeout'); }, 0); setImmediate(() => {console.log('setImmediate'); });
Can you guess the output above? Well, you might think that setTimeout would be printed first, but there's no guarantee, why? After executing the main module and entering the timer phase, he may not or may find that your timer has expired. why? A timer script is registered based on the system time and the delta time you provide. At the same time as setTimeout is called, the timer script is written to memory, and there may be a small delay depending on the performance of your machine and other operations (not nodes) running on it. At another point, node only sets a variable now before entering the timer phase (each round of traversal), using now as the current time. So you could say there's something wrong with the equivalent of precise time. This is the reason for uncertainty. If you point to the same code in a callback of a timer code you will get the same result.
However, if you move this code into the I/O cycle, it is guaranteed that the setImmediate callback will run before setTimeout.
fs.readFile('my-file-path.txt', () => { setTimeout(() => {console.log('setTimeout');}, 0); setImmediate(() => {console.log('setImmediate');}); });
var i = 0; var start = new Date(); function foo () { i++; if (i <p>The above example is very simple. Call function foo and then call foo recursively through setImmediate until 1000. On my computer, it took about 6 to 8 milliseconds. Fairy, please modify the above code and replace setImmedaite(foo) with setTimeout(foo, o). </p><pre class="brush:php;toolbar:false">var i = 0; var start = new Date(); function foo () { i++; if (i <p>Right now it takes 1400 ms to run this code on my computer. Why is this happening? None of them have i/o events, they should be the same. The wait event for the above two examples is 0. Why does it take so long? Deviations were found through event comparison, with CPU-intensive tasks taking more time. Registered timer scripts also consume events. Each stage of a timer requires some operations to be performed to determine whether a timer should execute. Longer execution also results in more ticks. However, in setImmediate, there is only one phase of checking, as if it were in a queue and then executed. </p><h5><strong>Fragment 3 - Understanding nextTick() & timer (timer) execution</strong></h5><pre class="brush:php;toolbar:false">var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000);
What do you think the above output is? Yes, it will print foo and then setTimeout. After 2 seconds, foo() is called recursively by nextTickQueue to print out the first foo. When all nextTickQueue are executed, other (such as setTimeout callbacks) will be executed.
So after each callback is executed, nextTickQueue starts to be checked? Let’s change the code and take a look.
var i = 0; function foo(){ i++; if (i>20) return; console.log("foo"); setTimeout(()=>console.log("setTimeout"), 0); process.nextTick(foo); } setTimeout(foo, 2000); setTimeout(()=>{console.log("Other setTimeout"); }, 2000);
After setTimeout, I just added another setTimeout that outputs Other setTimeout with the same delay time. Although not guaranteed, it is possible that Other setTimeout will be output after the first foo is output. The same timer is grouped into a group, and nextTickQueue will be executed after the callback group in progress is executed.
Most of us think of the event loop as being in a separate thread, pushing callbacks into a queue, and then executing them one after another. Readers who read this article for the first time may be wondering, where is Javascript executed? As I said earlier, there is only one thread, and Javascript code from the event loop itself using V8 or other engines is also run here. Execution is synchronous and the event loop will not propagate if the current Javascript execution has not completed.
First of all, it is not 0, but 1. When you set a timer and the time is less than 1, or greater than 2147483647ms, it will automatically be set to 1. Therefore, if you set the delay time of setTimeout is 0, it will automatically be set to 1.
In addition, setImmediate will reduce extra checks. Therefore setImmediate will execute faster. It is also placed after the polling phase, so the setImmediate callback from any incoming request will be executed immediately.
setImmediate and process.nextTick() are both misnamed. So functionally, setImmediate is executed on the next tick, and nextTick is executed immediately.
Because nextTickQueue has no limit on callback execution. So if you execute process.nextTick() recursively, your program may never get out of the event loop, no matter what you have in other stages.
It may initialize the timer, but the callback may never be called. Because if node is in the exit callback stage, it has already jumped out of the event loop. So there is no going back to execute.
The event loop does not have a work stack
The event loop is not in a separate thread, and the execution of Javascript is not like from the queue It is as simple as popping up a callback to execute.
setImmediate does not push the callback to the head of the work queue, there is a dedicated stage and queue.
setImmediate is executed in the next loop, and nextTick is actually executed immediately.
Be careful, nextTickQueue may block your node code if called recursively.
Related recommendations:
In-depth analysis of Node.js event loop_node.js
Introduction to the life cycle of JS controls_javascript Skill
The above is the detailed content of Detailed explanation of Node.js event loop workflow and life cycle. For more information, please follow other related articles on the PHP Chinese website!