JavaScript Eventloop Explained in Detail

Posted on 2022-03-11  1 View


The eventloop mechanism is key to Javascript's implementation of pseudo-"multithreading." This part is also a question that is often asked in interviews, take a note for reference.

The execution model of JavaScript

To be clear, JavaScript is a single-threaded language. All tasks in JavaScript are divided into one of three categories:

  • Synchronization tasks
  • Asynchronous macro tasks
  • Asynchronous microtasks

JavaScript implements asynchronous tasks by frequently switching between these tasks.

JavaScript's task execution mechanism

In terms of the overall execution mechanism, JavaScript handles the task at hand in the following order:

  • If it is a synchronous task, it is executed directly in the main thread.
  • If it is an asynchronous task, register the function in the Event Table, and when the asynchronous event completes (I/O, timeout, etc.), move the one-step callback function into the Event Queue.
  • When the main thread task finishes executing, start executing the asynchronous callback task in the Event Queue (the same EventLoop is always the first microtask and then the macro task) until the execution is completed.
  • This cycle is called Event Loop.

Macro tasks? Microtasks?

How to distinguish

  • Macro tasks
    • setTimeout, setInterval, setImmediate
  • Microtasks
    • Native Promise
    • process.nextTick(node.js)
    • Object.observe
    • MutationObserver

Enforcement mechanisms

Asynchronous events in an Event Queue are executed according to the following flowchart:

Asynchronous task execution process
  • First take a macro task from the macro task queue in the Event Queue and execute it.
  • After each macro task is executed, check the microtask queue and execute all microtasks.

Some examples

Easy difficulty

console.log(1)

new Promise((resolve, reject) => {
    console.log(2)
    resolve()
}).then(res => {
    console.log(3)
})

console.log(4)

Output: 1 2 4 3

Interpretation:

  • First, execute the synchronization task console.log(1) and output 1.
  • When a new Promise is encountered, immediately execute console.log(2) output 2, and register the .then callback function in the Event Queue.
  • Continue with the synchronization task console.log(4) and output 4.
  • After the synchronization task is executed, execute the microtask console.log(3) and output 3.

Medium difficulty

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
}, 0)

new Promise((resolve) => {
  console.log('promise')
  resolve()
}).then(() => {
    console.log('then1')
  }).then(() => {
    console.log('then2')
  })

console.log('end')

Output: start promise end then1 then2 setTimeout

Interpretation:

  • First, execute the synchronization task console.log (start) and output start.
  • When setTimeout is encountered, put the callback into the macro task queue.
  • When a new promise is encountered, immediately execute the console.log (promise) output the promise, and put two .then callback functions into the microtask queue.
  • Continue with the synchronization task console.log(end) and output end.
  • After the synchronization task is executed, execute the microtask console.log(then1, then2), and output then1, then2.
  • Finally, execute the macro task console.log (setTimeout) and output setTimeout.

Complex difficulty

console.log('start')

setTimeout(() => {
  console.log('setTimeout')
  new Promise((resolve) => {
      console.log('promise2')
      resolve()
  }).then( () => {
      console.log('then3')
  })
}, 0)

new Promise((resolve) => {
  console.log('promise1')
  resolve()
}).then(() => {
    console.log('then1')
  }).then(() => {
    console.log('then2')
  })

console.log('end')

Output: start promise1 end then1 then2 setTimeout promise2 then3

Interpretation:

  • First, execute the synchronization task console.log (start) and output start.
  • When setTimeout is encountered, put the callback into the macro task queue.
  • When a new promise is encountered, immediately execute console.log(promise1) to output promise1, and put two .then callback functions into the microtask queue.
  • Continue with the synchronization task console.log(end) and output end.
  • After the synchronization task is executed, execute the microtask console.log(then1, then2), and output then1, then2.
  • Execute the macro task console.log (setTimeout) and output setTimeout.
  • Execute the new promise from the previous macro task, immediately execute the console.log ('promise 2'), output promise 2, and put .then into the microtask queue.
  • Execute the microtask console.log (then3) and output then3.

Hell difficulty

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')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

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

Output: 1 7 6 8 2 4 3 5 9 11 10 12

Interpretation:

  • First, execute the synchronization task console.log(1) and output 1.
  • When setTimeout is encountered, put the callback into the macro task queue.
  • When you encounter process.nextTick, put the function in the microtask queue.
  • When a new Promise is encountered, immediately execute console.log(7) output 7, and put the .then callback function into the microtask queue.
  • When setTimeout is encountered, put the callback into the macro task queue.
  • After the synchronization task is executed, all microtasks are executed sequentially, with output 6,8.
  • Execute the first setTimeout macro task console.log(2) and output 2.
  • When you encounter process.nextTick, put the function in the microtask queue.
  • When a new promise is encountered, immediately execute the console.log(4) output 4, and put the .then callback function into the microtask queue.
  • After the macro task is executed, all microtasks are executed sequentially, with an output of 3,5.
  • Execute the second setTimeout macro task console.log(9) and output 9.
  • When you encounter process.nextTick, put the function in the microtask queue.
  • When a new Promise is encountered, immediately execute the console.log(11) output 11, and put the .then callback function into the microtask queue.
  • When the macro task is executed, all microtasks are executed sequentially, with output 10, 12.

Postscript

Recently encountered a related issue while dealing with react-redux's useSelector and useState hooks combined. Multiple setStates in the same useEffect, and the initial state and final state coincide, do not trigger the corresponding dependencies in other hooks. After using the new Promise(resolve=> setTimeout(resolve, 0)), the problem no longer occurs, which triggers the motivation to write this note. Speculation may be related to React's reconciliation mechanism, and it is necessary to dig deeper into React's code.