“This is the first day of my participation in the August More Text Challenge. For details, see: August More Text Challenge juejin.cn/post/698796…
1. Asynchronous execution principle
(1) Single-threaded JavaScript
As we know, JavaScript is a single-threaded language that is primarily used to interact with users and manipulate the DOM.
JavaScript has the concept of synchronous and asynchronous, which solves the blocking problem:
- Synchronization: A function is synchronous if the caller gets the expected result when it returns.
- Asynchronous: A function is asynchronous if the caller cannot get the desired result by the time it returns, but needs to get it by some means in the future.
So what are the benefits of a single thread?
- This may prevent UI rendering while JS is running, indicating that the two threads are mutually exclusive. This is because JS can modify the DOM, and if the UI thread is still working while JS is executing, the UI may not be rendered safely.
- Because JS is run in a single thread, you can achieve the benefits of memory saving and context switching time.
(2) Multi-threaded browsers
JS is single-threaded and can only do one thing at a time, so why can browsers perform asynchronous tasks at the same time?
This is because the browser is multi-threaded, and when javascript needs to perform an asynchronous task, the browser will start a separate thread to perform that task. In other words, JavaScript is single-threaded, meaning that there is only one thread that executes JavaScript code, which is the JavaScript engine thread provided by the browser (the main thread). In addition, the browser also has the timer thread, HTTP request thread and other threads, these threads are not mainly to execute JS code.
For example, if the main thread needs to send data request, it will hand this task to the asynchronous HTTP request thread to execute. After the request data returns, it will hand over the JS callback to the JS engine thread to execute. In other words, the browser is the one that actually performs the task of sending the request, and JS is only responsible for performing the final callback processing. So the asynchrony here is not implemented by JS itself, but is provided by the browser.
Here is the architecture of the Chrome browser:
As you can see, Chrome has not only multiple processes, but also multiple threads. Take the rendering process as an example, including GUI rendering thread, JS engine thread, event triggered thread, timer triggered thread, asynchronous HTTP request thread. These threads provide the foundation for JS to accomplish asynchronous tasks in the browser.
2. Browser event loop
JavaScript tasks fall into two categories: synchronous and asynchronous:
- Synchronous task: A task that is queued on the main thread so that only one task can be executed before the next task can be executed.
- Asynchronous tasks: do not enter the main thread, but in the task queue, if there are multiple asynchronous tasks need to wait in the task queue, the task queue is similar to the buffer, the task will be next moved to the execution stack and the main thread to execute the call stack task.
Task queues and execution stacks are mentioned above, so let’s take a look at these two concepts.
(1) Execution stack and task queue
1) Execution stack: As can be seen from the name, execution stack uses the stack structure in the data structure. It is a stack structure to store function calls, following the principle of first in, then out. It is mainly responsible for keeping track of all the code to be executed. Each time a function completes, the completion function is popped off the stack; Push if there’s any code that needs to be executed. Here is an example:
When we execute this code, we first execute a main function and then execute our code. According to the principle of “in, out”, the function that is executed later will pop up the stack first. As you can see in the figure, the function foo will be executed later, and when it is finished, it will pop up from the stack.
JavaScript executes methods on the execution stack sequentially, generating a unique execution context (context) for each method. When the method completes, it destroys the current execution environment, pops the method off the stack, and continues to execute the next method.
2) Task queue: As can be seen from the name, task queue uses the queue structure in the data structure, which is used to save asynchronous tasks, following the first-in, first-out principle. It is mainly responsible for sending new tasks to the queue for processing.
JavaScript executes code by placing the synchronized code on the execution stack and executing the functions in turn. When an asynchronous task is encountered, it is put into the task queue, waiting for the completion of all synchronous code execution of the current execution stack, it will take the callback of the completed asynchronous task from the asynchronous task queue and put it into the execution stack to continue execution, so the cycle, until all tasks are completed.
JavaScript tasks are executed in the following order:
In event-driven mode, at least one execution loop is included to detect whether there are new tasks in the task queue. This process is called event loop, and each loop is a cycle of events.
(2) Macro tasks and microtasks
In fact, there is more than one task queue, which can be divided into micro task queue and Macro task queue according to different types of tasks. Common tasks are as follows:
- Macro tasks: Script (overall code), setTimeout, setInterval, I/O, UI interaction events, setImmediate(node.js environment)
- Microtasks: Promise, MutaionObserver, Process. nextTick(node.js environment);
The task queue execution sequence is as follows:
As you can see, the Eventloop performs as follows when processing the logic of macro and microtasks:
- The JavaScript engine first takes the first task from the macro task queue;
- Micro task execution has been completed, and then take out all the tasks, according to the sequence of all executive (this includes not only refers to the micro tasks) began to perform in the queue, if produced in the process of this step is a new task, also need to perform, that is to say, in the process of executing the task of the new micro task will not be delayed until the next loop, Instead, execution continues within the current loop.
- The next macro task queue is removed from the macro task queue. After execution, the microTask queue is removed from the macro task queue again, and the loop repeats until the tasks in both queues are removed.
That is, an Eventloop handles one macro task and all microtasks generated in that loop.
Here’s an example to illustrate the event loop:
console.log('Sync code 1');
setTimeout(() = > {
console.log('setTimeout')},0)
new Promise((resolve) = > {
console.log('Sync code 2')
resolve()
}).then(() = > {
console.log('promise.then')})console.log('Sync code 3');
Copy the code
The output of the code is as follows:
"Sync code 1"
"Sync code 2"
"Sync code 3"
"promise.then"
"setTimeout"
Copy the code
So how does this code execute?
- When you encounter the first console, which is the sync code, add it to the execution stack, execute and exit the stack, and print “Sync code 1”;
- If you encounter setTimeout, which is a macro task, join the macro task queue;
- When you encounter the Console in the New Promise, which is the synchronization code, add it to the execution stack, execute and exit the stack, and print “Synchronization code 2”;
- When a Promise is encountered, it is a microtask and joins the microtask queue.
- When you encounter a third console, which is the sync code, add it to the execution stack, execute and exit the stack, and print “Sync code 3”;
- When the execution stack is empty, execute all tasks in the microtask queue and print “promise.then”;
- After executing the tasks in the microtask queue, it executes one of the tasks in the macro task queue, printing “setTimeout”
Note: For more examples of asynchronous code execution, see “2021” code Output Results.
From the workflow of macro and micro tasks above, the following conclusions can be drawn:
- Microtasks and macro tasks are bound, and each macro task creates its own microtask queue as it executes.
- The execution duration of a microtask affects the duration of a macro task. For example, during the execution of a macro task, 10 microtasks are generated. The execution time of each microtask is 10ms, so the execution time of these 10 microtasks is 100ms. In other words, the execution time of these 10 microtasks is extended by 100ms.
- In a macro task, create a macro task and a microtask for callbacks. In any case, the microtask executes before the macro task (with higher priority).
So why divide task queues into microtasks and macro tasks, and what are the essential differences between them?
When JavaScript encounters an asynchronous task, it assigns the task to another thread (such as the setTimeout task, it assigns the timer triggering thread to execute the task, and when the timer ends, it puts the timer callback task into the task queue for the main thread to fetch and execute), and the main thread continues to execute the subsequent synchronous task.
For a microtask such as promise.then, when promise.then is executed, the browser engine does not assign the asynchronous task to another browser thread for execution. Instead, the browser engine calls the task back to a queue, and when the task in the execution stack is finished, it executes the microtask queue in which promise.then is executed.
Therefore, the essential differences between macro and microtasks are as follows:
- Microtasks: no specific asynchronous threads are required to execute them, there are no specific asynchronous tasks to execute, only callbacks;
- Macro task: requires a specific asynchronous thread to execute, there are specific asynchronous tasks to execute, there are callbacks;
3. Node.js event loop
(1) The concept of event loops
The node.js event loop is described on the official website as follows:
When Node.js starts, it initializes the event loop, processes the provided input script (or drops into the REPL, which is not covered in this document) which may make async API calls, schedule timers, or call
process.nextTick()
, then begins processing the event loop.
When node.js starts, it initializes an event loop to process the input script. This script may make an asynchronous API call, schedule a timer, or call process.nexttick (), and then process the event loop.
JavaScript and Node.js are v8-based, and the asynchrony included in the browser is the same in NodeJS. In addition, there are several other forms of asynchrony in Node.js:
-
File I/O: Loads local files asynchronously.
-
SetImmediate () : Similar to setTimeout setting 0ms, perform certain synchronization tasks immediately after completion.
-
Process.nexttick () : Executes immediately after some synchronization tasks are completed.
-
Server. The close, the socket. On (‘ close ‘,…). And so on: close callback.
The execution of these asynchronous tasks relies on node.js event loops.
The Event Loop in Node.js is a completely different thing from the one in the browser. Node.js uses V8 as the JS parsing engine, while I/O processing uses libuv designed by itself. Libuv is an event-driven cross-platform abstraction layer, which encloses some low-level features of different operating systems and provides a unified API externally.From the figure above, we can see how Node.js works:
- The V8 engine is responsible for parsing JavaScript scripts;
- After parsing the code, call the Node API;
- The Libuv library is responsible for the execution of the Node API. It assigns different tasks to different threads, forming an Event Loop, and returns the execution results to the V8 engine in an asynchronous way.
- The V8 engine returns the results to the user;
(2) The process of event loop
The event loop in the Libuv engine is divided into six stages, which run repeatedly in sequence. Whenever a phase is entered, the function is retrieved from the corresponding callback queue and executed. When the queue is empty or the number of callbacks executed reaches a threshold set by the system, the next stage is entered. Here is the flow of an Eventloop:
The entire process is divided into six phases, and an Eventloop can be considered to be executed only after each of these six phases has been executed. Here’s what they do:
timers
Phase: Perform the callback of timer (setTimeout, setInterval), controlled by the poll phase;I/O callbacks
Phase: Mainly executes system-level callback functions, such as TCP connection failure callback;idle, prepare
Phase: used only internally in Node.js and can be ignored.poll
Phase: polling waiting for events such as new links and requests, performing I/O callbacks, etc.check
Phase: Perform a callback to setImmediate();close callbacks
Phase: Executes the callback function to close the request, such as socket.on(‘close’,…)
Note: Each of the above phases will execute the task queue of the current phase, and then continue to execute the microtask queue of the current phase. Only when all the microtasks of the current phase are executed, will the next phase be entered. This is also the logical difference from the browser.
The most important of these is the fourth stage, poll, in which the system does two main things:
- The callback is performed back in the Timer phase
- The I/O callback is performed
If the timer is not set when entering this phase, the following will happen:
(1) 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;
(2) If the poll queue is empty, the following occurs:
- If the setImmediate callback needs to be performed, the poll phase stops and the callback goes to the check phase.
- If no setImmediate callback needs to be performed, waiting for the callback to be queued and executing the callback immediately, there’s also a timeout set to prevent waiting forever;
When the timer is set and the poll queue is empty, the poll queue determines whether there are timer timeouts, and if there are, the poll queue goes back to the timer phase to perform a callback.
The specific execution process of this process is shown in the figure below:
(3) Macro tasks and microtasks
There are also two types of asynchronous queues for node.js event loops: macro task queues and microtask queues.
- Common macro tasks include setTimeout, setInterval, setImmediate, Script (overall code), and I/O operations.
- Common microtasks: process.nexttick, new Promise().then(callback), etc.
(4) process. NextTick ()
Process.nexttick (), mentioned above, is a new task queue introduced to Node that executes immediately at the end of each of the above phases before moving on to the next phase.
The official node.js documentation explains it as follows:
process.nextTick()is not technically part of the event loop. Instead, thenextTickQueuewill be processed after the current operation is completed, regardless of the current phase of the event loop. Here, an operation is defined as a transition from the underlying C/C++ handler, and handling the JavaScript that needs to be executed.
For example, the following code:
setTimeout(() = > {
console.log('timeout');
}, 0);
Promise.resolve().then(() = > {
console.error('promise')
})
process.nextTick(() = > {
console.error('nextTick')})Copy the code
The output is as follows:
nextTick
promise
timeout
Copy the code
As you can see, process.nexttick () is the callback execution that takes precedence over the promise.
5) setImmediate and setTimeout
SetImmediate and setTimeout are similar, but the main difference is the timing of the call:
- 4. SetImmediate: Performed when the poll phase is complete, the check phase;
- SetTimeout: executes when the poll phase is idle and the set time is up, but it executes in the Timer phase;
For example, the following code:
setTimeout(() = > {
console.log('timeout');
}, 0);
setImmediate(() = > {
console.log('setImmediate');
});
Copy the code
The output is as follows:
timeout
setImmediate
Copy the code
In the execution of the above code, after the first round of the loop, setTimeout and setImmediate are added to the task queue for their respective phases. In the second round of the loop, the timers phase is first performed, and the timer queue callback is performed. Then no tasks are performed in the Pending Callbacks and poll phases, so the setImmediate callback is performed in the Check phase. So the final output is timeout and setImmediate.
4. Difference between Node and browser Event Loop
The difference between Node.js and browser Event loops is as follows:
- Node.js: MicroTask executes between stages of the event loop;
- Browser: MicroTask executes after event loop macroTask executes;
Nodejs and browser event loops are compared as follows:
- Execute global Script code (browser equivalent);
- Emptying the microtask queue: Note that Node has a special way of emptying the microtask queue. In the browser, we only have one microtask queue to be processed; But in Node, there are two types of microtask queues: next-tick queues and other queues. This next-tick queue is dedicated to converging asynchronous tasks assigned by Process. NextTick. When the queue is cleared, tasks in the next-tick queue are cleared first, and other microtasks are cleared later.
- Start the macro task. Note that Node performs macro tasks differently than the browser: in the browser, we queue up and execute macro tasks one at a time; In Node, we try to clear all tasks in the macro task queue for the current phase (unless the system limit is reached).
- Step 3 Start, enter 3 -> 2 -> 3 -> 2… The loop.