Before we get to EventLoop, let’s look at one problem

setTimeout(() = > {
    console.log(111);
}, 1000);

while (true) {
    console.log(22);
}
Copy the code

console.log(111); It never prints because javaScript is single threaded

Threads and processes

concept

We often say that JS is single-threaded, meaning that there is only one main thread in a process. So what is a thread? What is a process?

Officially, processes are the smallest unit of CPU allocation. Threads are the smallest unit of CPU scheduling. These two sentences are not easy to understand, so let’s take a look at the picture:

  • Processes are like factories in the figure, with their own factory resources
  • A thread is like a worker in the picture, where multiple workers work cooperatively in a factory in a 1:n relationship between factories. That is, a process consists of one or more threads, which are different lines of code execution within a process
  • The factory space is shared by workers, which indicates that the memory space of a process is shared and available to each thread
  • Multiple factories exist independently of each other

Multiprocess versus multithreading

  • Multi-process: Allowing two or more processes to run at the same time on the same computer system. The benefits of multiple processes are obvious. For example, you can listen to music while opening the editor and typing code, and the editor and the listening software’s processes do not regret interfering with each other
  • Multithreading: Multiple execution streams in a program, that is, different threads can be run simultaneously in a program to perform different tasks, that is, a single program is allowed to create multiple concurrent execution threads to complete their respective tasks.

In Chrome, for example, when you open a Tab page, you create a process that can have multiple threads (more on that below), such as a rendering thread, a JS engine thread, an HTTP request thread, and so on. When you make a request, you create a thread that can be destroyed when the request ends

What is single threading?

The main program has only one thread, that is, it can only perform a single task in a fragment at a time.

Why is JS single threaded?

The main purpose of JavaScript is to interact with the user and manipulate the DOM, and if one thread is performing a delete operation and one is performing a modify operation, there will be problems. Therefore, it can only be single-threaded, otherwise it can introduce a lot of complicated synchronization problems.

What does a single thread mean?

Single threading means that only one task can be executed at a time, and all tasks need to be queued until the first task is finished. If the first task takes a long time, the second task will have to wait forever. This can lead to wasted performance when IO operations are time-consuming but CPU idle.

How to solve the performance problem caused by single threading

The answer is asynchronous, where the main thread can simply suspend the pending task and run the next one first, regardless of the IO operation. When the IO operation returns the result, go back and continue the pending task. Thus, the tasks can be divided into two types, synchronous and asynchronous.

Browser kernel

To put it simply, the browser kernel takes the page content, organizes the information (applying CSS), calculates and combines the final output visual image results, and is often called a rendering engine.

The browser kernel is multi-threaded, under the control of the kernel threads cooperate with each other to keep synchronized, a browser usually consists of the following resident threads:

  • GUI rendering thread
  • JavaScript engine threads
  • Timing trigger thread
  • Event trigger thread
  • Asynchronous HTTP request threads

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 backflow is caused by some operation.
  • This thread is mutually exclusive with the JS engine thread. When the JS engine thread executes, the GUI rendering will be suspended. When the task queue is idle, the JS engine will execute the GUI rendering.

JS engine thread

  • The thread is of course primarily responsible for processing JavaScript scripts and executing code.
  • The JS engine is always waiting for a task to arrive in the queue and then processing it. At any time in a Tab page, there is a JS thread running the JS program.
  • Of course, this thread is mutually exclusive with the GUI rendering thread, and when the JavaScript engine thread takes too long to execute the script, the page rendering will block.

Timer trigger thread

  • The thread responsible for executing functions such as asynchronous timers, such as setTimeout and setInterval.
  • When the main thread executes the code in turn, it will give the timer to the thread for processing. After counting, the event triggering thread will add the event after counting to the tail of the task queue and wait for the JS engine thread to execute.
  • Browser timing counters are not counted by JavaScript engines (because JavaScript engines are single-threaded, blocking threads can affect timing accuracy)
  • So a single thread is used to count and trigger timing

Event trigger thread

  • Belongs to the browser, not the JS engine, and is used to control the event loop
  • When the JS engine executes the code block, the corresponding task is added to the event thread
  • When an event that meets the trigger condition is triggered, the thread adds the event to the end of the queue to be processed by the JS engine
  • Note: since JS is single threaded, all events in the queue must be queued for processing by the JS engine

Asynchronous HTTP request threads

  • A thread responsible for executing functions such as asynchronous requests, such as Promises, AXIos, Ajax, etc.
  • When the main thread executes the code in turn, it meets the asynchronous request and gives the function to the thread for processing. When the status code changes, if there is a callback function, the event-triggering thread will add the callback function to the tail of the task queue and wait for the JS engine thread to execute.

Event Loop in browser

Micro – Task and Macro – a Task

There are two types of asynchronous queues in an event loop: Macro (macro task) and Micro (micro task) queues. There can be multiple macro task queues and only one microtask queue

  • Common macro-tasks include setTimeout, setInterval, setImmediate, Script, I/O, UI rendering, etc.
  • Common micro-tasks include process.nexttick, new Promise().then(callback), MutationObserver(new HTML5 feature), etc

Event Loop Process parsing

A complete Event Loop process can be summarized as the following stages:

  • When the execution stack is 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 the Macro queue has only script scripts.

  • The global context (script tag) is pushed onto the execution stack to synchronize code execution. During execution, the task is determined to be synchronous or asynchronous, and new macro-tasks and micro-tasks can be generated by calling some interfaces, which are pushed into their respective task queues. The script will be removed from the macro queue after the synchronized code is executed. This process is essentially the execution and dequeuing of the macro task in the queue.

  • In the previous step, we assigned a macro-task. In this step, we dealt with a micro-task. But it’s important to note that when Macro-Task is out, tasks are executed one by one; Micro-tasks, on the other hand, are executed in teams. So, as we process the micro queue, we execute the tasks in the queue one by one and de-queue them until the queue is empty.

  • Perform render operations to update the interface

  • Check whether Web worker tasks exist and process them if so

  • The process repeats until both queues are empty

To summarize, each cycle is a process like this:

When a macro task completes, it checks to see if there is a microtask queue. If yes, all the tasks in the microtask queue are executed first. If no, the first task in the macro task queue is read. During the execution of the macro task, microtasks are added to the microtask queue one by one. When the stack is empty, the tasks in the microtask queue are read again, and so on.

Let’s look at an example to illustrate 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)
Copy the code

The final output is Promise1, setTimeout1, Promise2, setTimeout2

  • The macro task setTimeout2 is generated as the macro task setTimeout2 is generated as the macro task setTimeout2 is generated as the macro task setTimeout2 is generated as the macro task setTimeout2 is generated as the macro task setTimeout2

  • Then go to the macro task queue, macro task setTimeout1 before setTimeout2, macro task setTimeout1, output setTimeout1

  • When macro task setTimeout1 is executed, it generates the Promise2 microtask and puts it into the microtask queue. Then it emptying all tasks in the microtask queue and outputs the Promise2 microtask

  • After clearing all tasks in the microtask queue, it goes to the macro task queue again, this time using setTimeout2

Event Loop in Node

Introduction of the Node

The Event Loop in Node is a completely different thing from the Event Loop in the browser. Node.js uses V8 as the PARSING engine of JS, and libuv designed by itself is used for I/O processing. Libuv is a cross-platform abstraction layer based on event-driven, which encapsulates some low-level features of different operating systems and provides a unified API externally. The event loop mechanism is also implemented in it:

Node.js works as follows:

  • The V8 engine parses JavaScript scripts.
  • The parsed code calls the Node API.
  • The Libuv library is responsible for executing the Node API. It assigns different tasks to different threads, forming an Event Loop that asynchronously returns the execution results of the tasks to the V8 engine.
  • The V8 engine is returning the results to the user.

Six stages

The event loop in the Libuv engine is divided into six phases, which are repeated sequentially. Each time a phase is entered, the function is fetched from the corresponding callback queue and executed. The next phase occurs when the queue is empty or the number of callback functions executed reaches a threshold set by the system.

In the figure above, you can roughly see the sequence of events in node:

External input data –> Polling stage –> Check stage –> Close callback stage –> Timer detection stage –>I/O callback stage –> IDLE stage Prepare)–> Polling phase (repeated in this order)…

  • Timers stage: This stage performs the callback of timer (setTimeout, setInterval)
  • I/O Callbacks phase: Handles some of the few unexecuted I/O callbacks from the previous cycle
  • Idle, prepare: Used only internally
  • Poll phase: Fetch new I/O events, where node will block under appropriate conditions
  • Check phase: Perform the setImmediate() callback
  • Close Callbacks stage: Performs the close event callback of the socket

Note: None of the above six phases includes process.nexttick ()(described below)

We continue to detail the timers, Poll, and Check phases, as these are where most asynchronous tasks in daily development are handled.

timer

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.

poll

Poll is a crucial stage in which the system does two things

  • Callback back to the timer execution phase
  • Perform I/O callback

And if the timer is not set when entering this phase, the following two things will happen

  • 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. There is also a timeout setting to prevent waiting forever

Of course, if the timer is set and the poll queue is empty, the poll queue will determine whether there is a timer timeout, and if so, the callback will be returned to the timer phase.

The check phase

The setImmediate() callback is added to the Check queue, and the phase diagram of the Event loop shows that the check phase is executed after the poll phase.

Let’s start with an example:

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
Copy the code
  • When a macro task is executed (printing the start end and putting the two timers into the timer queue), the macro task is executed (the same as in the browser), so the promise3 is printed

  • Then enter the Timers phase, execute the timer1 callback function, print timer1, and place the promise.then callback into the MicroTask queue, and do the same for timer2, print timer2; This is quite different from the browser side. Several setTimeout/setInterval are executed successively in the Timers stage, unlike the browser side, which executes a micro task after each macro task (As for the difference between Node and browser Event Loop, More on this later).

Pay attention to the point

SetTimeout and setImmediate

They are very similar, but the main difference lies in the timing of the call.

  • SetImmediate setImmediate is designed to perform when the poll phase is complete, the Check phase;
  • The setTimeout design is executed when the poll phase is idle and the set time is up, but it is executed during the timer phase
setTimeout(function timeout () {
  console.log('timeout');
},0);
setImmediate(function immediate () {
  console.log('immediate');
});
Copy the code
  • For the above code, setTimeout may be executed before or after.

  • First setTimeout(fn, 0) === setTimeout(fn, 1), this is determined by the source code

    There is also a cost to entering the event loop, and if it takes more than 1ms to prepare, then the setTimeout callback is executed directly during the Timer phase

  • If the setup time is less than 1ms, then the setImmediate callback executes first

However, setImmediate is always used before setTimeout is used when the two are called inside the asynchronous I/O callback

const fs = require('fs')
fs.readFile(__filename, () = > {
    setTimeout(() = > {
        console.log('timeout');
    }, 0)
    setImmediate(() = > {
        console.log('immediate')})})// immediate
// timeout
Copy the code

In the code above, setImmediate always executes first. The IO callback is performed in the POLL phase. The queue is empty after the POLL callback is completed. SetImmediate does not mediate, so it jumps directly to the Check phase to perform the callback.

process.nextTick

This function is actually separate from the Event Loop and has its own queue. When each stage is complete, if there is a nextTick queue, it will empty all callback functions in the queue and take precedence over 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
Copy the code

The Event Loop of Node is different from that of the browser

In the browser environment, the microTask queue is executed after each MacroTask has been executed. In Node.js, microTasks are executed between phases of the event cycle, i.e. tasks in the MicroTask queue are executed after each phase is completed.

Let’s use an example to illustrate the difference:

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

Running result on the browser: Timer1 =>promise1=> Timer2 =>promise2

The browser process is as follows:

Node running result: Timer1 => Timer2 =>promise1=>promise2

  • The global script (main()) is executed, and the two timers are put into the timer queue one by one. After main() is executed, the call stack is idle, and the task queue starts to execute.

  • First enter the timers stage, execute the callback function of Timer1, print timer1, and place the promise1. Then callback into the MicroTask queue. Repeat the same steps to run timer2 and print Timer2.

  • Before the Event loop enters the next phase, all tasks in the MicroTask queue are executed and promisE1 and promisE2 are printed successively

The Node process is as follows:

Six, summarized

The execution timing of microTask queues varies between browsers and Nodes

  • On the Node side, microTasks execute between stages of the event cycle
  • On the browser side, microTask executes after macroTask in the event loop completes execution

practice

setTimeout(() = >{
   console.log(1)},0)
let a=new Promise((resolve) = >{
    console.log(2)
    resolve()
}).then(() = >{
   console.log(3) 
}).then(() = >{
   console.log(4)})console.log(5) 
Copy the code

Output 2,5,3,4,1

new Promise((resolve,reject) = >{
    console.log("promise1")
    resolve()
}).then(() = >{
    console.log("then11")
    new Promise((resolve,reject) = >{
        console.log("promise2")
        resolve()
    }).then(() = >{
        console.log("then21")
    }).then(() = >{
        console.log("then23")
    })
}).then(() = >{
    console.log("then12")})Copy the code

promise1,then11,promise2,then21,then12,then23

new Promise((resolve,reject) = >{
    console.log("promise1")
    resolve()
}).then(() = >{
    console.log("then11")
    return new Promise((resolve,reject) = >{
        console.log("promise2")
        resolve()
    }).then(() = >{
        console.log("then21")
    }).then(() = >{
        console.log("then23")
    })
}).then(() = >{
    console.log("then12")})Copy the code

The second THEN of a Promise hangs on the return value of the last THEN of the new Promise.

promise1,then11,promise2,then21,then23,then12

new Promise((resolve,reject) = >{
    console.log("promise1")
    resolve()
}).then(() = >{
    console.log("then11")
    new Promise((resolve,reject) = >{
        console.log("promise2")
        resolve()
    }).then(() = >{
        console.log("then21")
    }).then(() = >{
        console.log("then23")
    })
}).then(() = >{
    console.log("then12")})new Promise((resolve,reject) = >{
    console.log("promise3")
    resolve()
}).then(() = >{
    console.log("then31")})Copy the code

promise1,promise3,then11,promise2,then31,then21,then12,then23

async function async1() {
    console.log("async1 start");
    await  async2();
    console.log("async1 end");
}

async function async2() {
    console.log( 'async2');
}

console.log("script start");

setTimeout(function () {
    console.log("settimeout");
},0);

async1();

new Promise(function (resolve) {
    console.log("promise1");
    resolve();
}).then(function () {
    console.log("promise2");
});
console.log('script end'); 
Copy the code

script start,async1 start,async2,promise1,script end,async1 end,promise2,settimeout

Async1 can be viewed as follows

funcation async1(){
    console.log("async1 start");
    new Promise((resolve) = >{
     console.log( 'async2');
    }).then(() = >{
        console.log("async1 end"); })}Copy the code
async function async1() {
    console.log(1)
    await async2()
    console.log(2)
    return await 3
}
async function async2() {
    console.log(4)}setTimeout(function() {
    console.log(5)},0)

async1().then(v= > console.log(v))
new Promise(function(resolve) {
    console.log(6)
    resolve();
    console.log(7)
}).then(function() {
    console.log(8)})console.log(9)
Copy the code

1,4,6,7,9,2,8,3,5

confusion

We know that Promise itself is an asynchronous method and must be fetched at the end of the stack, so all return values must be wrapped with a layer of asynchronous setTimeout. The question is why a Promise’s resolve is a microtask wrapped by setTimeout, which is a macro task.

parsing

In modern browsers, there are two ways to generate microtasks.

  • The first method is to use MutationObserver to monitor a DOM node, and then modify the node through JavaScript, or add or delete some molecular nodes for the node. When the DOM node changes, the micro-task of recording the DOM changes will be generated.

  • The second approach is to use promises, which also produce microtasks when promise.resolve () or promise.reject () is called.

The ECMAScript specification makes it clear that promises must join Job Queues (also known as Microtasks) as Promise jobs. A Job Queue is a new concept that is culled in ES6. It is built on event loop queues.