MEMEPh. ideas that are worth sharing...

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:

 

2. Multiprocessing and Multithreading

 

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:

 

1. GUI rendering thread

 

2. JS engine thread

 

3. The timer triggers the thread

 

4. Event trigger thread

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

 

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 .

2. Event Loop process analysis

 

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

 

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:

 

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)...

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

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

 

 

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.

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:

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

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 .