What is the difference between browser and Node's Event Loop
Foreword
In this article, we will introduce the principle of JS to achieve asynchrony, and understand that the Event Loop is actually different in the browser and Node.
1. Threads and Processes
1. Concept
We often say that JS is executed in a single thread, which means that there is only one main thread in a process, so what exactly is a thread? What is a process?
The official statement is: a process is the smallest unit of CPU resource allocation; a thread is the smallest unit of CPU scheduling . These two sentences are not easy to understand, let's look at a picture first:
- The process is like the factory in the figure, which has its own exclusive factory resource.
- Threads are like the workers in the figure. Multiple workers work collaboratively in a factory. The relationship between factories and workers is 1:n. That is to say , a process consists of one or more threads, and threads are different execution routes of the code in a process ;
- The space of the factory is shared by workers, which means that the memory space of a process is shared, and each thread can use the shared memory .
- Multiple factories exist independently.
2. Multiprocessing and Multithreading
- Multi-process: At the same time, if two or more processes are allowed to be running in the same computer system. The benefits of multi-process are obvious. For example, you can open the editor and type code while listening to the song, and the processes of the editor and the listening software will not interfere with each other at all.
- Multithreading: The program contains multiple execution streams, that is, a program can run multiple different threads at the same time to perform different tasks, that is to say, a single program is allowed to create multiple parallel execution threads to complete their respective tasks.
Taking the Chrome browser as an example, when you open a Tab page, you actually create a process, and a process can have multiple threads (described in detail below), such as rendering threads, JS engine threads, HTTP request threads, etc. Wait. When you initiate a request, you actually create a thread, and when the request ends, the thread may be destroyed.
2. Browser Kernel
In simple terms, the browser kernel is the final output of visual image results by obtaining page content, organizing information (applying CSS), computing and combining, and is often referred to as a rendering engine.
The browser kernel is multi-threaded. Under the control of the kernel, each thread cooperates with each other to maintain synchronization. A browser usually consists of the following resident threads:
- GUI rendering thread
- JavaScript engine thread
- timed trigger thread
- event trigger thread
- Asynchronous http request thread
1. GUI rendering thread
- Mainly responsible for page rendering, parsing HTML, CSS, building DOM tree, layout and drawing, etc.
- This thread is executed when the interface needs to be redrawn or when a reflow is caused by some operation.
- This thread is mutually exclusive with the JS engine thread. When the JS engine thread is executed, the GUI rendering will be suspended. When the task queue is idle, the main thread will perform GUI rendering.
2. JS engine thread
- Of course, this thread is mainly responsible for processing JavaScript scripts and executing code.
- It is also mainly responsible for executing events that are ready to be executed, that is, when the timer count ends, or when the asynchronous request succeeds and returns correctly, it will enter the task queue in turn and wait for the execution of the JS engine thread.
- Of course, this thread is mutually exclusive with the GUI rendering thread. When the JS engine thread executes the JavaScript script for too long, the page rendering will be blocked.
3. The timer triggers the thread
- The thread responsible for executing functions such as asynchronous timers, such as: setTimeout, setInterval.
- When the main thread executes the code in sequence, it will hand over the timer to the thread for processing when it encounters the timer. When the count is completed, the event trigger thread will add the counted event to the end of the task queue and wait for the JS engine thread to execute.
4. Event trigger thread
- Mainly responsible for handing the prepared events to the JS engine thread for execution.
For example, when the setTimeout timer expires, an asynchronous request such as ajax is successful and a callback function is triggered, or when the user triggers a click event, the thread will add the events to be sent to the end of the task queue in turn, waiting for the execution of the JS engine thread.
5. Asynchronous http request thread
- A thread responsible for executing functions such as asynchronous requests, such as: Promise, axios, ajax, etc.
- When the main thread executes the code in sequence, and encounters an asynchronous request, it will hand over the function to the thread for processing. When monitoring the status code change, if there is a callback function, the event trigger thread will add the callback function to the end of the task queue and wait for the JS engine Thread execution.
3. Event Loop in the browser
1. Micro-Task and Macro-Task
There are two kinds of asynchronous queues in the browser-side event loop: macro (macro task) queue and micro (micro task) queue. There can be multiple macro task queues and only one micro task queue .
- Common macro-tasks such as: setTimeout, setInterval, script (overall code), I/O operations, UI rendering, etc.
- Common micro-tasks such as: new Promise().then(callback), MutationObserver(html5 new feature), etc.
2. Event Loop process analysis
- A complete Event Loop process can be summarized into the following stages:
- At the beginning of the execution stack empty, we can think of the execution stack as a stack structure that stores function calls, following the principle of first-in, last-out . The micro queue is empty, and there is only one script (the whole code) in the macro queue.
- The global context (script tag) is pushed onto the execution stack, synchronizing code execution. In the process of execution, it will determine whether it is a synchronous task or an asynchronous task. By calling some interfaces, new macro-tasks and micro-tasks can be generated, and they will be pushed into their respective task queues. After the synchronization code is executed, the script script will be removed from the macro queue. This process is essentially the process of executing and dequeuing the macro-task of the queue.
- In the previous step, we dequeued a macro-task, and in this step, we dealt with a micro-task. But it should be noted that: when the macro-task is dequeued, the tasks are executed one by one; and when the micro-task is dequeued, the tasks are executed one by one. Therefore, we deal with the micro queue step, which will execute the tasks in the queue one by one and dequeue it until the queue is emptied.
- Perform rendering operations, update the interface
- Check if there is a web worker task, if so, process it
- The above process repeats itself until both queues are emptied
Let's summarize, each loop is a process like this:
When a macro task is executed, it will check whether there is a micro task queue. If yes, execute all the tasks in the microtask queue first. If not, it will read the top task in the macrotask queue. In the process of executing the macrotask, if a microtask is encountered, it will be added to the microtask queue in turn. After the stack is empty, read the tasks in the microtask queue again, and so on.
Next, let's look at an example to introduce the above process:
Promise.resolve().then(()=>{
console.log('Promise1')
setTimeout(()=>{
console.log('setTimeout2')
},0)
})
setTimeout(()=>{
console.log('setTimeout1')
Promise.resolve().then(()=>{
console.log('Promise2')
})
},0)
The final output is Promise1, setTimeout1, Promise2, setTimeout2
- After the synchronization task (which belongs to the macro task) of the initial execution stack is completed, it will check whether there is a micro-task queue, which exists in the above question (there is only one), and then execute all tasks in the micro-task queue to output Promise1, and at the same time it will Generate a macro task setTimeout2
- Then go to check the macro task queue, the macro task setTimeout1 executes the macro task setTimeout1 before setTimeout2, and outputs setTimeout1
- When the macro task setTimeout1 is executed, the micro-task Promise2 will be generated and put into the micro-task queue. Then, all tasks in the micro-task queue will be cleared first, and Promise2 will be output.
- After clearing all the tasks in the micro task queue, it will go to the macro task queue to get another one, this time setTimeout2 is executed
4. Event Loop in Node
1. Introduction to Node
The Event Loop in Node and the browser are completely different things. Node.js uses V8 as the parsing engine of js, and uses libuv designed by itself for I/O processing. libuv is an event-driven cross-platform abstraction layer that encapsulates some underlying features of different operating systems and provides a unified API to the outside world. , the event loop mechanism is also its implementation (described in detail below).
The operating mechanism of Node.js is as follows:
- The V8 engine parses JavaScript scripts.
- The parsed code calls the Node API.
- The libuv library is responsible for the execution of the Node API. It assigns different tasks to different threads to form an Event Loop (event loop), and returns the execution result of the task to the V8 engine in an asynchronous manner.
- The V8 engine then returns the result to the user.
2. Six stages
The event loop in the libuv engine is divided into 6 stages, which run repeatedly in sequence. Whenever a certain stage is entered, the function will be taken out from the corresponding callback queue for execution. When the queue is empty or the number of executed callback functions reaches the threshold set by the system, it will enter the next stage.
From the above figure, you can roughly see the order of the event loop in node:
External input data --> polling stage (poll) --> check stage (check) --> close event callback stage (close callback) --> timer detection stage (timer) --> I/O event callback stage (I/O callbacks)-->idle stage (idle, prepare)-->polling stage (run repeatedly in this order)...
- timers stage: This stage executes the callback of timer (setTimeout, setInterval)
- I/O callbacks phase: process some few unexecuted I/O callbacks from the previous loop
- idle, prepare stage: only used internally by node
- poll phase: get new I/O events, node will block here under appropriate conditions
- check phase: execute the callback of setImmediate()
- close callbacks stage: execute the socket's close event callback
Note: The above six stages do not include process.nextTick() (described below)
Next , we will introduce these three stages in detail timers, because most of the asynchronous tasks in daily development are processed in these three stages.pollcheck
(1) timer
The timers stage executes the setTimeout and setInterval callbacks and is controlled by the poll stage.
Likewise, the time specified by the timer in Node is not an accurate time, it can only be executed as soon as possible .
(2) poll
poll is a crucial stage, in this stage, the system will do two things
1. Go back to the timer stage to execute the callback
2. Execute the I/O callback
And when entering this stage, if the timer is not set, the following two things will happen
- If the poll queue is not empty, the callback queue will be 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 be executed, the poll phase will stop and enter the check phase to execute the callback
- If there is no setImmediate callback to be executed, it will wait for the callback to be added to the queue and execute the callback immediately. There will also be a timeout setting to prevent it from waiting.
Of course, if the timer is set and the poll queue is empty, it will judge whether there is a timer timeout, and if so, it will return to the timer stage to execute the callback.
(3) check stage
The callback of setImmediate() will be added to the check queue. As can be seen from the stage diagram of the event loop, the execution order of the check stage is after the poll stage.
Let's look at an example first:
console.log('start')
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Promise.resolve().then(function() {
console.log('promise3')
})
console.log('end')
//start=>end=>promise3=>timer1=>timer2=>promise1=>promise2
- After the synchronization task (which belongs to the macro task) of the stack is executed at the beginning (the start end is printed out in turn, and the two timers are put into the timer queue in turn), the micro task will be executed first ( this is the same as the browser side ) , so print out promise3
- Then enter the timers stage, execute the callback function of timer1, print timer1, put the promise.then callback into the microtask queue, execute timer2 in the same steps, and print timer2; this is quite different from the browser side, there are several setTimeouts in the timers stage /setInterval will be executed in sequence , unlike the browser side, which executes a micro task after each macro task is executed (the difference between the Event Loop of Node and the browser will be described in detail below).
3. Micro-Task and Macro-Task
There are also two types of asynchronous queues in the Node-side event loop: macro (macro task) queue and micro (micro task) queue.
Common macro-tasks such as: setTimeout, setInterval, setImmediate, script (overall code), I/O operations, etc.
Common micro-tasks such as: process.nextTick, new Promise().then(callback), etc.
4. Notes
(1) setTimeout and setImmediate
The two are very similar, the difference is mainly in the timing of the call.
- The setImmediate design is executed when the poll phase is completed, that is, the check phase;
- setTimeout is designed to be executed when the poll stage is idle and the set time is reached, but it is executed in the timer stage
setTimeout(function timeout () {
console.log('timeout');
},0);
setImmediate(function immediate () {
console.log('immediate');
});
For the above code, setTimeout may be executed before or after.
First, setTimeout(fn, 0) === setTimeout(fn, 1), which is determined by the source code.
Entering the event loop also costs money. If it takes more than 1ms to prepare, it will be executed directly in the timer phase. setTimeout callback
If the preparation time is less than 1ms, then the setImmediate callback is executed first
But when the two are called inside the asynchronous i/o callback, setImmediate is always executed first, and then setTimeout is executed.
const fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// immediate
// timeout
In the above code, setImmediate always executes first. Because the two codes are written in the IO callback, the IO callback is executed in the poll stage. When the callback is executed, the queue is empty, and it is found that there is a setImmediate callback, so it directly jumps to the check stage to execute the callback.
(2) process.nextTick
This function is actually independent of the Event Loop. It has its own queue. When each stage is completed, if there is a nextTick queue, it will clear all the callback functions in the queue and execute it prior to other microtasks.
setTimeout(() => {
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
process.nextTick(() => {
console.log('nextTick')
})
})
})
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1
Five, Node and browser's Event Loop difference
In the browser environment, the task queue of the microtask is executed after each macrotask is executed. In Node.js, the microtask will be executed between the various stages of the event loop, that is, after a stage is executed, the tasks of the microtask queue will be executed .
Next we use an example to illustrate the difference between the two:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
Browser-side running results:timer1=>promise1=>timer2=>promise2
The processing process on the browser side is as follows:
There are two cases of Node side running results:
- If the node11 version executes a macro task (setTimeout, setInterval and setImmediate) in a stage, the micro-task queue is executed immediately, which is consistent with the browser-side operation, and the final result istimer1=>promise1=>timer2=>promise2
- If it is node10 and its previous version: it depends on whether the first timer is executed and whether the second timer is in the completion queue.
- If the second timer is not yet in the completion queue, the final result istimer1=>promise1=>timer2=>promise2
- If the second timer is already in the completion queue, the final result is timer1=>timer2=>promise1=>promise2(the following process explanation is based on this case)
1. The global script (main()) is executed, and the two timers are put into the timer queue in turn. After the main() is executed, the call stack is idle, and the task queue starts to execute;
2. First enter the timers stage, execute the callback function of timer1, print timer1, and put the promise1.then callback into the microtask queue, execute timer2 in the same steps, and print timer2;
3. At this point, the execution of the timer stage is over, and before the event loop enters the next stage, all tasks in the microtask queue are executed, and promise1 and promise2 are printed in turn.
The processing process on the Node side is as follows:
6. Summary
In the browser and Node environments, the execution timing of the microtask task queue is different
- On the Node side, microtasks are executed between stages of the event loop
- On the browser side, the microtask is executed after the macrotask of the event loop is executed
- postscript
Since the node version is updated to 11, the operation principle of Event Loop has changed. Once a macro task (setTimeout, setInterval and setImmediate) in a stage is executed, the micro task queue is executed immediately. Just like the browser side .