preface

I read a lot of articles when learning the browser and NodeJS Event Loop, and those articles are all well written. However, there are usually several key points in each article. Only when many articles are put together, can I have a deeper understanding of these concepts.

Therefore, after reading a lot of articles, I want to write such a blog, not using the official description, combined with their own understanding and sample code, expressed in the most popular language. In this article, you can see what Event Loop is and what the difference is between a browser and a NodeJS Event Loop. If there are writing mistakes in the article, welcome to leave a message to discuss.

(PS: When it comes to Event Loop, Promise will definitely be mentioned. I have implemented A simple Promise library according to the Promise A+ specification, and put the source code on Github. If you need it, you can refer to it as your reference. Please give a Star ~)

The body of the

What is an Event Loop

Event Loop is an execution model with different implementations in different places. Browsers and NodeJS implement their own Event loops based on different technologies.

  • The browser Event Loop is explicitly defined in the HTML5 specification.
  • NodeJS Event Loop is implemented based on Libuv. You can refer to the Node documentation and libuv documentation.
  • Libuv has made an implementation of Event Loop, while THE HTML5 specification only defines the model of Event Loop in browsers, and the specific implementation is left to the browser manufacturers.

Macro queues and microqueues

Macroqueue, macroTask, also known as Tasks. Callbacks from asynchronous tasks are placed in the Macro Task Queue, waiting to be called later.

  • setTimeout
  • setInterval
  • SetImmediate (Exclusive to Node)
  • RequestAnimationFrame (browser only)
  • I/O
  • UI Rendering (browser only)

Microqueues, microtasks, also known as Jobs. Other asynchronous tasks whose callbacks go to the Micro Task Queue, waiting to be called later, include:

  • Process. nextTick (Node only)
  • Promise
  • Object.observe
  • MutationObserver

(Note: this is for browsers and NodeJS only)

Event Loop for the browser

Let’s take a look at the picture first, and after reading this article, please go back and look at the picture again, I’m sure you’ll have a better understanding.

This picture fully describes the Event Loop of the browser. Let me tell you the specific process of executing a JavaScript code:

  1. Execute global Script synchronization code, some of which are synchronous statements, some of which are asynchronous statements (setTimeout, etc.);
  2. After executing the global Script code, the call Stack is emptied;
  3. Remove the callback task at the beginning of the microtask queue from the microtask queue and put it into the call Stack for execution. After execution, the length of the MicroTask queue decreases by 1.
  4. Continue fetching the task at the top of the queue, place it on the call Stack, and so on until all the tasks in the MicroTask Queue have been executed. Note that if a microtask is created in the process of executing it, it is added to the end of the queue and executed during this cycle.
  5. When all tasks in the MicroTask Queue are completed, the MicroTask Queue is empty and the call Stack is empty.
  6. Remove the task at the head of the macroTask queue and add it to the Stack for execution.
  7. After the execution, the call Stack is empty.
  8. Repeat steps 3-7;
  9. Repeat steps 3-7;
  10. .

As you can see, this is the Event Loop of the browser

Here are three key points:

  1. Macrotask takes one task from the queue at a time, and then executes the tasks in the microtask queue.
  2. All tasks in the microtask queue are fetched and executed until the microtask queue is empty.
  3. There is no UI rendering node because it is at the discretion of the browser, but once UI rendering is performed, the node performs UI render immediately after all the microtasks and before the next MacroTask.

Ok, that’s all for the conceptual stuff, let’s take a look at some sample code to test your grasp:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);
Copy the code

So what’s going to happen here? Use what you’ve learned above to try it out for yourself.

// The correct answer is 1 4 7 5 2 3 6Copy the code

Did you get that right?

Let’s analyze the process:


  1. Execute global Script code

Step 1

console.log(1)
Copy the code

Stack Queue: [console]

Macrotask Queue: []

Microtask Queue: []

Print result: 1

Step 2

setTimeout(() => {// This callback is called callback1,setTimeout belongs to macroTask, so put it in macroTask queue console.log(2); Promise.resolve().then(() => { console.log(3) }); });Copy the code

Stack Queue: [setTimeout]

Macrotask Queue: [callback1]

Microtask Queue: []

Print result: 1

Step 3

New Promise((resolve, reject) => {// Console. log(4) resolve(5)}). Then ((data) => {// This callback is called callback2, promise is a microtask, So put it in microTask queue console.log(data); })Copy the code

Stack Queue: [promise]

Macrotask Queue: [callback1]

Microtask Queue: [callback2]

Print result: 1 4

Step 5

setTimeout(() => {// This callback is called callback3,setTimeout belongs to macroTask, so put it in macroTask queue console.log(6); })Copy the code

Stack Queue: [setTimeout]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

Print result: 1 4

Step 6

console.log(7)
Copy the code

Stack Queue: [console]

Macrotask Queue: [callback1, callback3]

Microtask Queue: [callback2]

Print result: 1 4 7


  1. Now that the global Script is finished, proceed to the next step, fetching and executing tasks from the MicroTask Queue until the MicroTask Queue is empty.

Step 7

Console. log(data) // Where data is the Promise resolution value 5Copy the code

Stack Queue: [callback2]

Macrotask Queue: [callback1, callback3]

Microtask Queue: []

Print result: 1 4 7 5


  1. There is only one task in the MicroTask queue, and the task at the top of the queue is executed from the macroTask Queue

Step 8

console.log(2)
Copy the code

Stack Queue: [callback1]

Macrotask Queue: [callback3]

Microtask Queue: []

Print result: 1 4 7 5 2

However, another Promise is encountered when callback1 is executed, and the Promise executes asynchronously and registers a Callback4 callback in the MicroTask Queue

Step 9

Promise.resolve().then(() => {// This callback is called callback4, Promise belongs to microTask, so put it in microTask queue console.log(3)});Copy the code

Stack Queue: [promise]

Macrotask v: [callback3]

Microtask Queue: [callback4]

Print result: 1 4 7 5 2


  1. After removing a macrotask and executing it, remove it from the microtask queue

Step 10

console.log(3)
Copy the code

Stack Queue: [callback4]

Macrotask Queue: [callback3]

Microtask Queue: []

Print result: 1 4 7 5 2 3


  1. After all the tasks in the micro task queue are executed, the first task in the macro task queue will be executed

Step 11

console.log(6)
Copy the code

Stack Queue: [callback3]

Macrotask Queue: []

Microtask Queue: []

Print result: 1 4 7 5 2 3 6


  1. The Stack Queue is empty, Macrotask Queue is empty, and Micro Queue is empty

Stack Queue: []

Macrotask Queue: []

Microtask Queue: []

Final print result: 1 4 7 5 2 3 6

Since this is the first example, the analysis here is more detailed. Let’s take a closer look. Next, let’s take another example:

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

Copy the code

What is the final output? Consider the previous example and consider……

// The correct answer is 1Copy the code

I’m sure you’re all right. The key here has already been mentioned:

If a microtask is created during the execution of a task in the microTask queue, it is added to the end of the queue and executed in this cycle until the MicroTask queue is empty and stops.

Note: Of course, if you constantly create microtasks in microTask, other macrotasks will not be able to execute, but this operation is not infinite. Take the microtask process.nexttick () in NodeJS, which is capped at 1000, as we will discuss later.

So much for the Event Loop in the browser, let’s take a look at the Event Loop in NodeJS, which is a bit more complicated and has a different mechanism.

Event Loop in NodeJS

libuv

Take a look at the structure of libuv:

Macroqueues and microqueues in NodeJS

In NodeJS Event Loop, the macro queue callback task has 6 stages, as shown below:

The tasks performed in each phase are as follows:

  • Timers stage: This stage performs callback scheduled by setTimeout and setInterval
  • I/O Callback phase: Execute callbacks except for close events, callbacks set by Timers, and callbacks set by setImmediate()
  • Idle, prepare: Used only internally
  • Poll phase: Fetch new I/O events, where node will block under appropriate conditions
  • Check phase: Perform the Callbacks set by setImmediate()
  • Close Callbacks: execute socket.on(‘close’,….) These callbacks

There are four main macro queues in NodeJS

As you can see from the introduction above, callback events are located primarily in four MacroTask Queues:

  1. Timers Queue
  2. IO Callbacks Queue
  3. Check Queue
  4. Close Callbacks Queue

All four belong to macro queues, but in the browser, you can think of only one macro queue, and all macroTasks are added to that one macro queue, but in NodeJS, different MacroTasks are placed in different macro queues.

There are two microqueues in NodeJS:

  1. Next Tick Queue: This is where the process.nexttick (callback) callback is placed
  2. Other Micro Queue: Places Other microtasks, such as promises

In a browser, you can also think of a single microqueue to which all microtasks are added, but in NodeJS, different microtasks are placed in different microqueues.

You can deepen your understanding through the following figure:

An overview of NodeJS Event Loop:

  1. Synchronous code that executes the global Script
  2. To execute microtask tasks, execute all tasks in the Next Tick Queue and then all tasks in the Other MicroTask Queue
  3. The Event Loop in the browser is used to select only the first macrotask from the macrotask queue. The Event Loop in the browser is used to select the first macrotask from the macroTask queue. After each phase of the MacroTask is complete, start the microtask, that is, step 2
  4. Timers Queue -> Step 2 -> I/O Queue -> Step 2 -> Check Queue -> Step 2 -> Close Callback Queue -> Step 2 -> Timers Queue……
  5. This is the Node Event Loop

NodeJS macroTask Queue and MicroTask Queue I drew two diagrams for your reference:

Ok, concept understood let’s use a few examples to put it into practice:

The first example

console.log('start');

setTimeout(() => {          // callback1
  console.log(111);
  setTimeout(() => {        // callback2
    console.log(222);
  }, 0);
  setImmediate(() => { // callback3 console.log(333); }) process.nextTick(() => { // callback4 console.log(444); })}, 0);setImmediate(() => { // callback5 console.log(555); process.nextTick(() => { // callback6 console.log(666); })})setTimeout(() => { // callback7 console.log(777); process.nextTick(() => { // callback8 console.log(888); })}, 0); process.nextTick(() => { // callback9 console.log(999); }) console.log('end');
Copy the code


Update 2018.9.20

The result of executing the above code can be different for several reasons.

  • SetTimeout (fn, 0) is not strictly 0, but setTimeout(FN, 3) or something, and there are two situations where setTimeout(FN, 0) and setImmediate(FN) occur in the same synchronized code.

  • Case 1: The setImmediate callback registers with the Check Queue and begins to implement the microqueue and then the macro Queue. The setImmediate callback does not register with the Check Queue until the Timer expires. Then the timer expires (the effect is the same as long as the timer Queue expires). The timer callback is registered with the Timers Queue, and the timer callback is not executed until the next cycle is executed in the Timers Queue. So, in this case, the setImmediate(fn) callback executes before the setTimeout(fn, 0) callback.

  • The timer does not register the timer callback to the Timers Queue. SetImmediate mediate registers its callback to the Check Queue. The synchronization code is then completed and the microqueue is executed. Timers Queue is followed by Timer callbacks and Check Queue by setImmediate callbacks. So, in this case, the setTimeout(fn, 0) callback is executed before the setImmediate(fn) callback.

  • So, calling setTimeout(fn, 0) and setImmediate at the same time in synchronized code is uncertain, but if you place them in an IO callback, such as readFile(‘xx’, function () {//…. SetImmediate mediate does not register the content of an IO Queue. The setTimeout timeout callback registers with Timers Queue. SetImmediate does not register the content of an IO Queue. The timer Queue gets the next cycle, so the setImmediate callback must be executed before the setTimeout(fn, 0) callback in this case.

SetTimeout (fn, 0) and setImmediate(fn) mediate(fn, 0) do not mediate(FN, 0), but mediate(fn, 0) do not mediate(FN, 0). The result is unique.

Update the end



Please use the previous knowledge, carefully analyze……

Start end 999 111 777 444 888 555 333 666 222Copy the code

Did you get that right? Let’s break it down:

  1. To register the setTimeout callback callback1 in the Timers Queue, run down to register the setImmediate callback callback5 in the Check Queue. Proceed down to register setTimeout callback Callback7 to the Timers Queue, and proceed down to register process.nextTick callback callback9 to the microqueue Next Tick Queue, printing end as the last step. At this point, the callback of each queue is as follows:

Macro queue

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: [callback9]

Other Microtask Queue: []

Print the result start end

  1. After executing the global Script, start executing all the callback tasks in the Next Tick Queue. At this point, there is only one callback9 in the Next Tick Queue. Remove it from the call stack and execute it, printing 999.

Macro queue

Timers Queue: [callback1, callback7]

Check Queue: [callback5]

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: []

Other Microtask Queue: []

Start end 999

  1. Run all tasks in the macro queues of the six stages in sequence. Run all tasks in the Timers Queue of the first stage first. Run the callback1 function and print 111. Add Callback3 to the Check Queue, add Callback4 to the Next Tick Queue, and then callback1 completes. Then run the command to retrieve the callback7 that ranks first in the Timers Queue, print 777, and add callback8 to the Next Tick Queue. The operation is complete. At this point, the status of each queue is as follows:

Macro queue

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: [callback4, callback8]

Other Microtask Queue: []

Start end 999 111 777

  1. At this point, all tasks in the Next Tick Queue will be executed. Callback4 will execute and 444 will be printed. Then Callback8 will execute and 888 will be printed. Next Tick Queue Starts to execute tasks in Other Microtask Queue because the task is empty.

Macro queue

Timers Queue: [callback2]

Check Queue: [callback5, callback3]

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: []

Other Microtask Queue: []

Start end 999 111 777 444 888

  1. The second phase of the IO Callback Queue is empty, so the Queue is skipped. The third and fourth phases are usually used by nodes internally, so the Queue is skipped. Print 555, place callback6 in the Next Tick Queue, run callback3, print 333.

Macro queue

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: [callback6]

Other Microtask Queue: []

Start end 999 111 777 444 888 555 333

  1. To execute the Microtask Queue, run the Next Tick Queue command, print the callback6 command, and print 666. The execution is complete, because the Other Microtask Queue is empty, skip this task.

Macro queue

Timers Queue: [callback2]

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: [callback6]

Other Microtask Queue: []

Start end 999 111 777 444 888 555 333

  1. Execute phase 6, Close Callback Queue, empty, skip, and a loop has ended. Enter the next loop and execute all tasks in the first stage of Timers Queue. Run callback2 and print 222. At this point, all queues, including macro and micro task queues, are empty and no longer print anything.

Macro queue

Timers Queue: []

Check Queue: []

IO Callback Queue: []

Close Callback Queue: []

The queue

Next Tick Queue: [callback6]

Other Microtask Queue: []

End Result Start End 999 111 777 444 888 555 333 666 222

The above is the detailed analysis of this topic, if you do not understand, be sure to see several times.


Let’s introduce Promise as another example:

console.log('1');

setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
process.nextTick(function() {
  console.log('6');
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')})})Copy the code

In the Other Microtask Queue, there will be callback tasks because there are promises. In the Microtask phase, all tasks in the Next Tick Queue will be executed first. All tasks in the Other Microtask Queue are executed before the next phase of the macro task is performed. Understand this point, I believe that we can analyze it, below directly give the correct answer, if you have any questions, welcome to leave a message and I discuss.

// The correct answer is 1 7 6 8 2 4 9 11 3 10 5 12Copy the code

SetTimeout contrast setImmediate

  • SetTimeout (fn, 0) is executed in the Timers phase and is executed in the poll phase to determine whether the specified timer time has been reached
  • SetImmediate (FN) performs in the Check phase

The order of execution depends on the current execution environment:

  • If both are called from the main Module, the execution order is random, depending on process performance
  • If neither is called in the main module, that is, in an I/O Circle, then the setImmediate callback is always executed first because the Check phase is reached first

SetImmediate comparison process. NextTick

  • The setImmediate(FN) callback task is inserted into the macro Check Queue
  • The process.Nexttick (FN) callback task is inserted into the microqueue Next Tick Queue
  • The process.nexttick (fn) call is limited in depth to 1000, while setImmedaite is not

conclusion

  1. The browser Event Loop is different from the NodeJS Event Loop, and the implementation mechanism is different.
  2. NodeJS can be understood as having four macro task queues and two microtask queues, but there are six phases in macro task execution. The global Script code is executed first. After the synchronous code is executed and the call stack is cleared, all tasks are removed from the Next Tick Queue and put into the call stack for execution. Then, all tasks are removed from the Other Microtask Queue and put into the call stack for execution.Then start the task of macro six stages, each stage: remove all the task in the macro task queue to perform (note, here is different and the browser, the browser only take one), after each macro stage task execution, start executing the task, to start the next stage of macro tasks, in order to constitute the event loop.
  3. NodeJS can be understood as having four macro task queues and two microtask queues, but there are six phases in macro task execution. The global Script code is executed first. After the synchronous code is executed and the call stack is cleared, all tasks are removed from the Next Tick Queue and put into the call stack for execution. Then, all tasks are removed from the Other Microtask Queue and put into the call stack for execution. Then the macro task starts six phases, each of which executes all tasks in the macro task queue (note that unlike the browser, the browser only takes one task). After the execution of the six phases, the micro task starts to execute, thus forming an event loop.
  4. MacroTask includes: setTimeout, setInterval, setImmediate(Node), requestAnimation(browser), IO, UI Rendering
  5. Microtasks include process.nextTick(Node), Promise, Object.observe, and MutationObserver

3. In the new version, Node executes microTasks after each Macrotask is executed, which is consistent with the browser model.

Welcome to follow my public number

Refer to the link

Don’t confuse NodeJS with event loops in browsers

The Event module in Node

Promises, process.nextTick And setImmediate

Browser and Node have different event loops

Tasks, microtasks, queues and schedules

Understand the cycle of events