preface
As we all know, javascript has been a single-threaded, non-blocking scripting language since its inception. This is determined by its original purpose: interacting with browsers.
-
Single-threaded means that at any point in your javascript code’s execution, there is only one main thread to handle all the tasks.
-
Non-blocking is when the code needs to perform an asynchronous task (such as an I/O event), the main thread will suspend the task, and then execute the corresponding callback when the asynchronous task returns the result according to certain rules.
One of the reasons single-threading is necessary and a cornerstone of the javascript language is that in its original and primary execution environment, the browser, we need to do all sorts of DOM manipulation. If javascript is multithreaded, what happens when two threads simultaneously perform an operation on the DOM, such as one adding an event to it and the other deleting the DOM? Therefore, to ensure that a scenario like the one in this example does not occur, javascript chooses to execute the code with only one main thread, thus ensuring consistent program execution.
Of course, nowadays, people also realize that single thread not only ensures the execution order but also limits the efficiency of javascript, so Web Worker technology is developed. This technology claims to make javascript a multithreaded language.
However, multi-threading using Web Worker technology has many limitations. For example, all new threads are under the complete control of the main thread and cannot be executed independently. This means that these “threads” actually belong to the child threads of the main thread. In addition, these child threads do not have access to I/O operations and can only share some tasks such as computation with the main thread. So strictly speaking these threads are not fully functional, and therefore this technology does not change the single-threaded nature of the javascript language.
In the future, javascript will always be a single-threaded language.
Another feature of javascript mentioned earlier is that it is “non-blocking”, so how exactly does a javascript engine achieve this? The answer is the subject of today’s post — the Event loop.
Note: Although there are similar event loops in NodeJS to those found in traditional browser environments. However, there are many differences between the two, so separate them and explain them separately.
Event loops in the browser
Js execution order introduction
Whether you’re interviewing for a job, or in your daily development work, it’s common to find that given a few lines of code, you need to know what to output and in what order. Since javascript is a single-threaded language, we can conclude that:
- Javascript is executed in the order in which the statements appear
Because js is executed line by line, we expect js to look like this:
let a = '1';
console.log(a);
let b = '2';
console.log(b);
Copy the code
However, js actually looks like this:
setTimeout(function(){
console.log('Timer's on.')});new Promise(function(resolve){
console.log('Execute for loop now');
for(var i = 0; i < 10000; i++){
i == 99 && resolve();
}
}).then(function(){
console.log('Execute the then function')});console.log('End of code execution');
Copy the code
Given the idea that JS executes in the order in which the statements appear, does its output look like this?
//" Timer started"
//" Execute for loop now"
//" execute then"
//" Code execution complete"
Copy the code
Let’s open Chrome to verify:
It can be found that the results of our previous speculation are completely wrong ❗
At this point, it should be obvious that we need to understand javascript execution mechanisms once and for all.
◾ about javascript
Javascript is a single-threaded language. Web-worker was proposed in the latest HTML5, but the core that javascript is single-threaded remains unchanged. So all javascript version of the “multithreading” are simulated with a single thread, all javascript multithreading is a paper tiger!
Event Loop
Since JS is a single thread, js tasks must be executed sequentially. If one task takes too long, the next must also wait. So the question is, if we want to browse the news, but the news contains an ultra hd image that loads slowly, should our web page stay stuck until the image is fully displayed? Of course not. Js tasks fall into two categories:
- Synchronization task
- Asynchronous tasks
When we open a website, the rendering process of the page is a lot of synchronization tasks, such as rendering the page skeleton and page elements. Asynchronous tasks, such as loading images and videos, consume a lot of resources and take a long time.
The picture above contains several points:
- Synchronous and asynchronous tasks go to different execution “places”, synchronous tasks go to the main thread, asynchronous tasks go to the Event Table and register functions.
- When the specified Event completes, the Event Table moves this function to the Event Queue
(Event queue)
. - If the main thread execution stack is empty, the Event Queue will read the corresponding function and enter the main thread execution.
- This process is repeated over and over again, known as an Event Loop.
How do you know that the main thread stack is empty? The js engine has a monitoring process that continuously checks to see if the main thread stack is empty, and if it is, checks to see if there are any functions waiting to be called in the Event Queue.
Execution stack and event queue
As javascript code executes, different variables are stored in different locations in memory: the heap and stack. There are some objects in the heap. The stack holds some basic type variables and Pointers to objects. But the execution stack we’re talking about here has a slightly different meaning than the one above.
As we know, when we call a method, JS generates an execution context corresponding to the method, also called the execution context. The execution environment contains the private scope of the method, the pointer to the upper scope, the method parameters, the variables defined in the scope, and the this object in the scope. When a series of methods are called in sequence, js is single-threaded and only one method can be executed at a time, so the methods are queued in a separate place. This place is called the execution stack.
When a script is executed for the first time, the JS engine parses the code, adds the synchronized code to the stack in the order it is executed, and then executes it from scratch. If a method is currently executing, js adds the method’s execution environment to the execution stack, and then enters the execution environment to continue executing the code. When the code in this execution environment completes and returns the result, js exits the execution environment and destroys the execution environment, returning to the execution environment of the previous method. This process is repeated until all the code in the execution stack has been executed.
The following image shows this process visually, where global is the code added to the stack when the script is first run:
As you can see from the picture, a method execution adds its execution environment to the execution stack. In this execution environment, other methods, or even itself, can be called, which simply adds another execution environment to the execution stack. This process can go on indefinitely, unless a stack overflow occurs, that is, the maximum amount of memory available is exceeded.
The above procedure is all about synchronous code execution. What happens when asynchronous code, such as sending Ajax request data, is executed? As mentioned earlier, another feature of JS is non-blocking, and the key to achieving this is the mechanism described below: Task Queue.
Instead of waiting for an asynchronous event to return, the JS engine suspends the event and continues to execute other tasks in the stack. When an asynchronous event returns a result, JS adds the event to a different queue from the current stack, called the event queue. Instead of executing its callback immediately, it waits for all tasks in the current execution stack to complete, and when the main thread is idle, it looks up whether there are any tasks in the event queue. If so, the main thread will fetch the first event, place the corresponding callback on the stack, and execute the synchronization code. And so on and so on and so on and so on and so on and so on and so on and so on. This is why the process is called an Event Loop.
Here’s another diagram to illustrate the process:
The stack represents what we call the execution stack, the Web apis represent asynchronous events, and the callback queue is the event queue.
◾ Event queue
All tasks can be divided into synchronous tasks and asynchronous tasks. Synchronous tasks, as the name implies, are immediately executed tasks. Generally, synchronous tasks are directly executed in the main thread. Asynchronous tasks are asynchronously executed tasks, such as Ajax network requests and setTimeout timing functions. Asynchronous tasks are coordinated through the mechanism of Event Queue. The specific figure can be used to roughly illustrate:
Synchronous and asynchronous tasks enter different execution environments. Synchronous tasks enter the main thread (main execution stack) and asynchronous tasks enter the Event Queue. If the task execution in the main thread is empty, the Event Queue will read the corresponding task and push the main thread to execute it. The repetition of this process is known as an Event Loop.
Macro tasks and Micro Tasks
The above event loop is a macro statement, but because asynchronous tasks are different from one another, their execution priorities are different. Different asynchronous tasks are divided into two categories: micro tasks and macro tasks.
The following events are macro tasks:
setInterval()
setTimeout()
The following events are microtasks:
promise.then()
Async/Await(actually promise)
new MutaionObserver()
As described earlier, in an event loop, asynchronous events return results that are placed in a task queue. However, depending on the type of asynchronous event, the event can actually be queued to the corresponding macro or microtask queue. And when the current stack is empty, the main thread checks for events in the microtask queue. If not, fetch an event from the macro task queue and add the corresponding event back to the current stack. If so, the queue will execute the corresponding callback until the microtask queue is empty, then fetch the first event from the macro task queue, and add the corresponding callback to the current stack. And so on and so on and so on.
We just need to remember that when the current stack finishes, all events in the microtask queue are processed immediately, and then an event is fetched from the macro task queue. Microtasks are always executed before macro tasks in the same event loop.
◾ this will explain the result of this code:
setTimeout(function () {
console.log(1);
});
new Promise(function(resolve,reject){
console.log(2)
resolve(3)
}).then(function(val){
console.log(val);
})
Copy the code
The result is:
2
3
1
Copy the code
Example explanation
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function () {
console.log('promise1');
}).then(function () {
console.log('promise2');
});
console.log('script end');
Copy the code
Does the output look like this?
script start
promise1
promise2
script end
setTimeout
Copy the code
No!!
The correct output is:
script start
script end
promise1
promise2
setTimeout
Copy the code
It is forgotten that the main thread checks for events in the microtask queue only when the current stack is empty.
Console. log(‘script end’); This code must be executed first, and then check if there are any microtasks. This should pay attention to, do not accidentally ignore ~
Let’s go through the above example step by step and post the example code first (so you don’t have to scroll up) :
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
Copy the code
- The whole script enters the main thread as the first macro task, encounters console.log, and prints script start
- When a setTimeout is encountered, its callback function is dispatched to the macro task Event Queue
- When a Promise is encountered, its then function is assigned to the micro-task Event Queue, named THEN1, and then it is encountered and assigned to the micro-task Event Queue, named then2
- When console.log is encountered, print script end
Here, the output is:
script start
script end
Copy the code
So far, there are three tasks in the Event Queue, as shown in the following table:
The following:
- Execute then1 to output promise1, then execute then2 to output promise2, thus emptying all microtasks
- Execute the setTimeout task and print setTimeout, thus clearing out all macro tasks
At this point, the final output order is:
script start
script end
promise1
promise2
setTimeout
Copy the code
◾ If you are already familiar with the road, you might as well take a look at the following question:
(function test() {
setTimeout(function() {console.log(4)}, 0);
new Promise(function executor(resolve) {
console.log(1);
for( var i=0 ; i<10000 ; i++ ) {
i == 9999 && resolve();
}
console.log(2);
}).then(function() {
console.log(5);
});
console.log(3); }) ()Copy the code
The output is:
1
2
3
5
4
Copy the code
queueMicrotask()
It passes in JavaScriptqueueMicrotask()
Use microtasks
A microtask is a short function that executes after the function that created it, and only when the Javascript call stack is empty and control has not been returned to the event loop used by the User Agent to drive the script execution environment. The microtask will be executed. An event loop can be either the browser’s main event loop or one driven by a Web worker. This allows a given function to run without interference from other script execution and ensures that the microtask can run before the user agent has a chance to react to the behavior that the microtask brings.
A user agent is a computer program that represents a person, for example, a browser on the Web
Both the Promise and Mutation Observer apis in JavaScript use microtask queues to run their callback functions, but when it is possible to defer work until the current event loop is complete, it is also possible to perform microtasks. To allow third-party libraries, frameworks, and polyfills to use microtasks, Window exposes the queueMicrotask() method, While the Worker interface by WindowOrWorkerGlobalScope mixin provides the same queueMicrotask () method.
◾ Tasks vs microtasks
In order to properly discuss microtasks, it’s good to know what a JavaScript task is and how a microtask differs from a task.
Forced the task(Tasks)
A task is any JavaScript code that is scheduled by performing standard mechanisms such as executing a program from scratch, executing an event callback, or triggering an interval/timeout. These are all scheduled on the task queue.
Tasks are added to the task queue when:
- When a new program or subroutine is executed directly (e.g., from a console, or in a
<script>
Element to run code. - Triggers an event to add its callback function to the task queue.
- Execute to a by
setTimeout()
或setInterval()
To create thetimeout
或interval
So that the corresponding callback function is added to the task queue.
An event loop drives your code to process these tasks one by one in the order in which they are queued. In the current iteration round, only those tasks that are already in the task queue when the event loop process begins will be executed. The rest of the tasks have to wait until the next iteration.
Forced the micro tasks(Microtasks)
At first the differences between microtasks and tasks seem small. They’re very similar; Are made up of JavaScript code that sits on a queue and runs when appropriate. However, only tasks that are in the queue at the beginning of the iteration are run one after another by the event loop, which is quite different from working with microtask queues.
There are two key differences.
First, every time a task exists, the event loop checks to see if the task is ceding control to other JavaScript code. If not, the event loop runs all the microtasks in the microtask queue. The microtask loop is then processed multiple times in each iteration of the event loop, including after the event and other callbacks are processed.
Second, if a microtask adds more microtasks to the queue by calling queueMicrotask(), those newly added microtasks will run before the next task. This is because the event loop keeps calling microtasks until there are no remaining microtasks in the queue, even if more microtasks keep being added.
◾ Use microtasks
Before going any further, it is important to note again that most developers should not use microtasks too much if at all possible. One highly specialized feature of modern browser-based JavaScript development is that it allows you to schedule code to jump ahead of other things that are already in a large set of things waiting to happen on the user’s computer. Abusing this capability can cause performance problems.
As such, microtasks should typically be used, either when there is no other way, or when a framework or library needs to be created for its functionality. While techniques have been available in the past to make inline microtasks possible (such as creating a promise to resolve immediately), the addition of the queueMicrotask() method adds a standard way to safely introduce microtasks without the need for additional techniques.
By introducing queueMicrotask(), the risk of using promises arcane to create microtasks can be avoided. For example, when you create a microtask using Promise, the exception thrown by the callback is reported as Rejected Promises instead of the standard exception. At the same time, creating and destroying promises introduces additional overhead in terms of events and memory that properly included microtask functions should avoid.
Simply pass in a JavaScript Function to be called in context when processing microtasks in the queueMicrotask() method; Depending on the current execution context, queueMicrotask() is exposed as defined on the Window or Worker interface.
queueMicrotask(() = > {
/* The code to run in the microtask */
});
Copy the code
Microtask functions themselves take no arguments and return no values.
▪ When to use microtasks
In this chapter, we look at scenarios where microtasks are particularly useful. Often, these scenarios are about capturing or examining results, performing cleanup, and so on; This happens later than the exit of a JavaScript execution context body, but before any event handlers, timeouts, intervals, and other callbacks are executed.
When is that useful?
The primary reason for using microtasks is simply to ensure consistency in the order of tasks, even when results or data are available synchronously, while reducing the risk of perceived delays in operations.
◾ Simple microtask example
In this simple example, we will see that enlisting a microtask causes its callback function to run after the top-level script completes.
In the code below, we see that a call to queueMicrotask() is used to schedule a microtask to run. This call contains log(), a simple custom function that outputs text to the screen.
log("Before enqueueing the microtask");
queueMicrotask(() = > {
log("The microtask has run.")}); log("After enqueueing the microtask");
Copy the code
Results:
Before enqueueing the microtask
After enqueueing the microtask
The microtask has run.
Copy the code
Examples of ◾ timeout and microtasks
In this example, a timeout is triggered after 0 milliseconds (or “as soon as possible”). This demonstrates what “as soon as possible” means when calling a new task (such as by using setTimeout()), and the difference compared to using a microtask.
In the code below, we see that a call to queueMicrotask() is used to schedule a microtask to run. This call contains log(), a simple custom function that outputs text to the screen.
The following code dispatches a timeout that fires after 0 milliseconds and enlists a microtask. It is wrapped in calls to log(), which outputs additional information.
let callback = () = > log("Regular timeout callback has run");
let urgentCallback = () = > log("*** Oh noes! An urgent callback has run!");
log("Main program started");
setTimeout(callback, 0);
queueMicrotask(urgentCallback);
log("Main program exiting");
Copy the code
Results:
Main program started
Main program exiting
*** Oh noes! An urgent callback has run!
Regular timeout callback has run
Copy the code
Notice that the log output from the main program body appears first, followed by the output from the microtask, followed by the timeout callback. This is because when the task handling the main program runs exits, the microtask queue is processed before the task queue where the timeout callback is located. Keep in mind that tasks and microtasks remain separate queues, and executing the microtasks first helps keep this in mind.
◾ from function microtasks
This example slightly extends the previous example by adding a function that does the same job. This function uses queueMicrotask() to schedule a microtask. The important thing about this example is that the microtask is not executed when the function it is in exits, but when the main program exits.
Here is the main program code. The doWork() function here calls the queueMicrotask(), but the microtask still fires when the entire program exits, because that’s when the task exits and the execution stack is empty.
let callback = () = > log("Regular timeout callback has run");
let urgentCallback = () = > log("*** Oh noes! An urgent callback has run!");
let doWork = () = > {
let result = 1;
queueMicrotask(urgentCallback);
for (let i=2; i<=10; i++) {
result *= i;
}
return result;
};
log("Main program started");
setTimeout(callback, 0);
log(` 10! equals${doWork()}`);
log("Main program exiting");
Copy the code
Results:
Main program started
10! equals 3628800
Main program exiting
*** Oh noes! An urgent callback has run!
Regular timeout callback has run
Copy the code
Event loop in node
How it differs from the browser environment
In Node, the event loop behaves roughly the same as in the browser. The difference is that Node has its own model. The implementation of event loops in Node relies on the Libuv engine.
As we know, Node selects Chrome V8 engine as the JS interpreter. V8 engine analyzes THE JS code and calls the corresponding Node API, which is finally driven by libuv engine to perform the corresponding tasks and put different events in different queues waiting for the main thread to execute. So the event loop in Node actually exists in the Libuv engine.
Macro and micro tasks in Node
Node also has macro tasks and microtasks, which are similar to event loops in browsers:
setTimeout()
setInterval()
setImmediate()
I/O operations
Micro-tasks include:
process.nextTick()
(Different from normal microtasks, executed before the microtask queue)promise.then()
And so on.
From the official Node.js event loop introduction
The Node.js event loop is one of the most important aspects of understanding Node.js. Why is it so important? Because it illustrates how Node.js can be asynchronous and have non-blocking I/O, it basically illustrates node.js as a “killer app.”
Node.js JavaScript code runs on a single thread. Deal with one thing at a time.
This limitation is actually quite useful because it greatly simplifies programming without worrying about concurrency.
Just be careful how you code and avoid anything that might block the thread, such as synchronous network calls or infinite loops.
In general, in most browsers, each browser TAB has an event loop to keep each process isolated and to avoid using infinite loops or heavy processing to block the entire browser web page.
This environment manages multiple concurrent event loops, such as handling API calls. The Web worker process also runs in its own event loop, as discussed earlier.
The call stack
The call stack is a LIFO queue (last in, first out).
The event loop constantly checks the call stack to see if any functions need to be run.
When executed, it adds all the function calls it finds to the call stack and executes each function in order.
Do you know the error stack trace you might be familiar with in a debugger or browser console? The browser looks up the function name in the call stack to tell you which function initiated the current call:
A simple illustration of the cycle of events
Here’s an example:
const bar = () = > console.log('bar')
const baz = () = > console.log('baz')
const foo = () = > {
console.log('foo')
bar()
baz()
}
foo()
Copy the code
This code prints as expected:
foo
bar
baz
Copy the code
When this code is run, foo() is called first. Inside foo(), bar() is called first, followed by baz().
At this point, the call stack looks like this:
The event loop in each iteration looks to see if there is something in the call stack and executes it until the call stack is empty:
The enqueue function executes
The example above looks normal, nothing special: JavaScript looks for things to execute and runs them in order.
Let’s see how to defer the function until the stack is cleared.
The use case for setTimeout(() => {}, 0) is to call a function, but after every other function in the code has been executed.
Here’s an example:
const bar = () = > console.log('bar')
const baz = () = > console.log('baz')
const foo = () = > {
console.log('foo')
setTimeout(bar, 0)
baz()
}
foo()
Copy the code
The code prints:
foo
baz
bar
Copy the code
When this code is run, foo() is called first. Inside foo(), setTimeout is called first, passing bar as an argument and 0 as a timer to tell it to run as quickly as possible. Then call baz().
At this point, the call stack looks like this:
This is the order in which all the functions in the program are executed:
Why is that?
The message queue
When setTimeout() is called, the browser or Node.js starts the timer. When the timer expires (immediately in this case, because the timeout value is set to 0), the callback function is put into the “message queue.”
In message queues, user-triggered events, such as clicks or keyboard events, or getting responses, are also queued here before the code has a chance to react to them. The same is true for DOM events like onLoad.
The event loop gives priority to the call stack, processes everything it finds in the call stack first, and once there is nothing in it, processes everything in the message queue.
◾ ES6 job queue
ECMAScript 2015 introduced the concept of job queues, which Promise uses (also introduced in ES6/ES2015). In this way, the result of an asynchronous function is executed as quickly as possible, rather than at the end of the call stack.
The Promise of resolve before the end of the current function is executed immediately after the current function.
Example:
const bar = () = > console.log('bar')
const baz = () = > console.log('baz')
const foo = () = > {
console.log('foo')
setTimeout(bar, 0)
new Promise((resolve, reject) = >
resolve('Should be after Baz and before bar')
).then(resolve= > console.log(resolve))
baz()
}
foo()
Copy the code
This will print:
Foo baz should come after baz and before bar barCopy the code
This is the big difference between Promises (and async/await built on Promise) and plain old asynchronous functions via setTimeout() or other platform apis.
To understand the process. NextTick ()
One important part of trying to understand the Node.js event loop is process.nexttick ().
Each time an event loop completes a trip, we call it a tick.
When a function is passed to process.nexttick (), it instructs the engine to call the function at the end of the current operation (before the next event loop tick begins) :
process.nextTick(() = > {
// Do something
})
Copy the code
The event loop is busy processing the current function code. When the operation is complete, the JS engine runs all the functions passed to the nextTick call during the operation.
Calling setTimeout(() => {}, 0) executes the function at the end of the nextTick, much later than using nextTick(), which takes precedence over the call and executes the function before the nextTick begins.
NextTick () is used when ensuring that the code has been executed in the next iteration of the event loop.
Understand setImmediate ()
One option when executing some code asynchronously (but as quickly as possible) is to use the setImmediate() function provided by Node.js:
setImmediate(() = > {
// Run something
})
Copy the code
Any function passed in as the setImmediate() parameter is a callback that is executed in the next iteration of the event loop.
SetImmediate () Differences between setTimeout() and process.nexttick ()
SetImmediate () How does setImmediate() differ from setTimeout(() => {}, 0) (passing 0 ms timeout), process.nexttick ()?
The function passed to process.nexttick () is executed during the current iteration of the event loop (after the current operation ends). This means that it will always execute before setTimeout and setImmediate.
The 0 millisecond delay setTimeout() callback is very similar to setImmediate(). The order of execution depends on various factors, but they all run in the next iteration of the event loop.
Node event loop model
Here is a model of the event loop in the Libuv engine. The following diagram shows a simplified overview of the sequence of event loop operations:
┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ > │ timers │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ pending Callbacks │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ │ idle, Prepare │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ incoming: │ │ │ poll │ < ─ ─ ─ ─ ─ ┤ connections, │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ data, Etc. │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ check │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┬ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┴ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ └ ─ ─ ┤ close callbacks │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘Copy the code
Note: Each square in the model represents a phase of the event cycle
This model is a simplified diagram of the Node event cycle presented in an article on the Node website
Each box in the figure is referred to as a phase of the event loop mechanism, and each phase has a FIFO queue to perform the callback. While each phase is special, typically, when an event loop enters a given phase, it will perform any operation specific to that phase and then execute the callbacks in that phase’s queue until the queue is exhausted or the maximum number of callbacks has been executed. When the queue is exhausted or the callback limit is reached, the event loop moves to the next phase.
Note:First-in, first-out queue (First Input First Output, FIFO
), which is a traditional sequential execution method in which the first entry instruction is completed and retired, followed by the second instruction.
Therefore, from the simplified figure above, we can analyze the stage sequence of node event cycle as follows:
Incoming data -> Poll -> Check -> Close callback -> Timers ->I/O event callback (I/O Callbacks -> Idle -> polling…
◾ Phase Overview
- Timers: This stage performs the callback of timer, that is, the callback functions of setTimeout and setInterval.
- I/O event callback phase (I/O callbacks) : I/O callbacks that are deferred until the next iteration of the loop, i.e., some I/O callbacks that were not executed in the previous loop.
- Idle, prepare: Used for internal use only.
- Poll: Retrieves new I/O events; Perform I/ O-related callbacks (in almost all cases, except for closed callback functions, those scheduled by timers and setImmediate()), where Node will block at the appropriate time.
- Check phase: The setImmediate() callback is performed here
- Close callback: Some closed callback functions, such as socket.on(‘close’,…) .
◾ Three key stages
Most asynchronous tasks in daily development are handled in the poll, Check, and Timers phases, so let’s focus on them.
Forced the timers
The Timers phase performs setTimeout and setInterval callbacks and is controlled by the Poll phase. Similarly, the timer specified in Node is not the exact time and can only be executed as soon as possible.
Forced the poll
Poll is a crucial stage. The execution logic flow chart of poll stage is as follows:
If a timer already exists and a timer is running out, the eventLoop returns to the Timers phase.
If there is no timer, it will look at the callback function queue.
▪ If the poll queue is not empty, the callback queue is traversed and executed synchronously until the queue is empty or the system limit is reached
▪ If the poll queue is empty, two things happen
- If there is a setImmediate callback to perform, the poll phase stops and enters the Check phase to perform the callback
- If no setImmediate callback needs to be executed, the state waits for the callback to be added to the queue and then executes it immediately. The state also has a timeout setting that prevents the state from waiting and then entering the Check phase.
Go check
The check phase. This is a simpler stage, where the setImmdiate callback is performed directly.
Forced process. NextTick
Process. nextTick is a task queue independent of eventLoop.
After each eventLoop phase is complete, the nextTick queue is checked and, if there are tasks in it, those tasks take precedence over microtasks.
Look at an example:
setImmediate(() = > {
console.log('timeout1')
Promise.resolve().then(() = > console.log('promise resolve'))
process.nextTick(() = > console.log('next tick1'))}); setImmediate(() = > {
console.log('timeout2')
process.nextTick(() = > console.log('next tick2'))}); setImmediate(() = > console.log('timeout3'));
setImmediate(() = > console.log('timeout4'));
Copy the code
(4) Before Node11, the nextTick queue is checked after each eventLoop phase, and the task, if any, takes precedence over the microtask. Thus, the code starts with the Check phase and executes all setImmediate. The nextTick queue is executed after completion, and finally the microtask queue is executed, so the output is:
timeout1
timeout2
timeout3
timeout4
next tick1
next tick2
promise resolve
Copy the code
▪ After Node11, process.nextTick is a microtask, so the above code enters the Check phase, executes a setImmediate macro task, then executes its microtask queue, and then executes the next macro task and its microtask, so the output is:
timeout1
next tick1
promise resolve
timeout2
next tick2
timeout3
timeout4
Copy the code
We can take a look at the above code in the Node environment:
Note: My node version isv14.15.4
Node Version Differences
The main difference here is before and after Node11, because some of the features have been brought into line with the browser, and the overall change is, in a word, The Node11 version does not perform a single macro task (setTimeout,setInterval, and setImmediate) that executes the corresponding microtask queue
◾ Execution timing of the timers phase changes
setTimeout(() = >{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')})},0)
setTimeout(() = >{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')})},0)
Copy the code
Execution in Node (Node version V14.15.4) :
▪ For node11 and above, setTimeout,setInterval, and setImmediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate mediate Mediate Mediate Mediate Mediate Mediate Mediate Mediate Mediate Mediate Mediate
timer1
promise1
timer2
promise2
Copy the code
▪ Node10 or earlier depends on whether the first timer is finished and the second timer is in the completion queue.
- If the second timer is not in the completion queue, the final result is
timer1=>promise1=>timer2=>promise2
- If the second timer is already in the completion queue, the final result is
timer1=>timer2=>promise1=>promise2
The execution timing changes for the ◾ Check phase and nextTick queue are similar and will not be described here.
It should be clear from the above examples that node11 and later versions of mediate perform the corresponding microtask queue as soon as a macro task (setTimeout,setInterval, and setImmediate) is performed in a phase.
reference
- Explain the Event Loop mechanism in JavaScript
- Node.js event loop (nodejs.cn)
- In-depth understanding of JavaScript event loops
- Using microtasks in JavaScript with queueMicrotask() -mdn (mozilla.org)
- This time, thoroughly understand the JavaScript execution mechanism
- Interview question: Tell me about the cycle of events.
- Learn the Event Loop once (Once and for all)
- Microtasks, macro tasks, and Event-loops