An article to understand the running mechanism of js – event loop

1. Explain the execution mechanism of JavaScript.

The execution mechanism of JavaScript is based on the event loop. The event loop includes a task queue (Task Queue) and a microtask queue (Microtask Queue). When a function is called, it is added to the microtask queue. Each iteration of the event loop will first execute all tasks in the microtask queue, and then execute a macrotask (Macrotask), such as script code, setTimeout, setInterval, setImmediate, I/O, UI rendering, etc. When a macrotask is completed, the event loop will check the microtask queue again and execute all microtasks. This process will continue looping forever, so it is called an “event loop”.

2. Explain the event loop in JavaScript.

The event loop is the core running mechanism of JavaScript, which is responsible for scheduling and executing all tasks. The event loop includes a task queue (Task Queue) and a microtask queue (Microtask Queue). When a function is called, it is added to the microtask queue. Each iteration of the event loop will first execute all tasks in the microtask queue, and then execute a macrotask (Macrotask), such as script code, setTimeout, setInterval, setImmediate, I/O, UI rendering, etc. When a macrotask is completed, the event loop will check the microtask queue again and execute all microtasks. This process will continue looping forever, so it is called an “event loop”.

3. Explain the task queue and microtask queue in JavaScript.

Task Queue and Microtask Queue are both important concepts in the event loop. The task queue is used to store macro tasks (Macrotask), such as script code, setTimeout, setInterval, setImmediate, I/O, UI rendering, etc. When a macro task is added to the task queue, it will be executed in the next event loop. The microtask queue is used to store microtasks, such as Promise callback functions, process.nextTick, etc. Microtasks are executed immediately after the current event loop’s macrotask has completed execution, regardless of when they are added to the microtask queue.

4. Why does the execution of setTimeout(fn, 0) occur in the next event loop?

The execution of setTimeout(fn, 0) will occur in the next round of event loop, because setTimeout adds function fn to the task queue, and the macro task in the task queue will be executed in the next event loop. Since the delay parameter of setTimeout is 0, fn will be executed immediately in the next event loop.

5. What are macro tasks and micro tasks? What’s the difference between them?

Macrotask and microtask are two different types of tasks in the event loop.
Macro tasks includethe entire script code, setTimeout, setInterval, setImmediate, I/O, UI rendering, etc.
Microtasks include Promise callback functions, process.nextTick, etc.
The difference between them lies in the timing and order of execution. Each time the event loop iterates, a macrotask and all microtasks will be executed, but the execution of the macrotask will interrupt the execution of the microtask. Therefore, microtasks have higher execution priority than macrotasks.

6. Explain how Promise.then and Promise.catch work in JavaScript? How do they relate to the event loop?

Promise.then and Promise.catch are important functions in JavaScript for handling asynchronous operations. A Promise object represents the eventual completion (or failure) of an asynchronous operation and its result value.

The Promise.then method returns a new Promise object called thenable. thenable represents a Promise-like object that can be processed like a Promise. When the Promise completes, the thenable’s callback function will be triggered.

The Promise.catch method returns a Promise that fires when the first rejected Promise or thenable rejects.

Promise.then and Promise.catch are both related to event loops. In the execution model of JavaScript, there is a task queue (task-queue) and a microtask queue (microtask-queue). When an asynchronous event (such as timer, Ajax request) completes, its callback function will be added to the microtask queue. In the next loop, JavaScript will process all tasks in the microtask queue before processing tasks in the task queue. The callback functions of Promise.then and Promise.catch will wait for execution in the microtask queue. When the Promise is completed or rejected, the corresponding callback function will be added to the microtask queue and executed in the next loop.

7. Explain MutationObserver in JavaScript and its relationship with the event loop.

MutationObserver is an interface that provides the ability to monitor DOM tree changes. It is designed to provide low-latency notifications when the DOM tree changes. The relationship between MutationObserver and the event loop is that its callback function waits for execution in the microtask queue. When the DOM tree changes, the MutationObserver’s callback function will be added to the microtask queue and executed in the next loop.

8. Describe the call stack and event loop in JavaScript.

In JavaScript, execution context is a concept used to describe the environment in which JavaScript code runs. When JavaScript code is executed, a new execution environment is created, and this environment exists until the code execution is completed. The execution environment mainly includes call stack and heap.

The call stack is a last-in-first-out (LIFO) data structure used to store function calls currently executing. When a function is called, it is added to the top of the call stack. When the function completes execution, it is removed from the call stack.

The event loop is a cyclic process that continuously checks whether the call stack is empty. If the call stack is not empty, the event loop will take out a task (which can be a macrotask or a microtask) from the call stack, execute it, and then add the result to the microtask queue. Then, the event loop checks whether the microtask queue is empty. If the microtask queue is not empty, the event loop will remove all microtasks from the microtask queue and execute them. The event loop then checks again to see if the call stack is empty and repeats the process.

9. What is the JavaScript event loop? Please briefly describe how it works.

The event loop is the core execution mechanism of JavaScript. It consists of one or more loops, each loop is called a loop or an iteration. In each loop, the event loop will perform a series of tasks in a certain order, including processing user interaction, executing script code, processing network requests, etc.

The event loop works as follows:

  1. Execute a single loop: The event loop starts executing a single loop. In a single loop, the event loop will perform a series of tasks in a certain order, including processing user interaction, executing script code, processing network requests, etc. These tasks are called macro-tasks, including script (overall code), setTimeout, setInterval, setImmediate, I/O, UI rendering, etc. At the end of a single loop, the event loop checks the microtask-queue.
  2. Processing microtasks: If there are tasks in the microtask queue, the event loop will execute all microtasks immediately until the microtask queue is empty. Microtasks include Promise.then, process.nextTick, MutationObserver, etc. It is important to handle microtasks at the end of a single loop because they are often associated with asynchronous operations that need to be executed immediately after the current synchronous operation.
  3. Check the termination condition: If the current single loop has executed all macrotasks and microtasks, and no other synchronization operations need to be performed, the event loop will end the current single loop and start the next single loop. Otherwise, the event loop continues to wait for the next macrotask to arrive and starts the next one-shot loop.

10. What is the difference between microtask and macrotask in the event loop?

In the JavaScript event loop, tasks are divided into two types: microtask and macrotask.

Macrotask: Macrotask refers to tasks that take a long time to complete, such as script (whole code), setTimeout, setInterval, setImmediate, I/O, UI rendering, etc.

Microtask: Microtask is a JavaScript task whose execution priority is higher than macrotask. Microtasks include but are not limited to Promise callback functions, MutationObserver callback functions, etc.

The main difference between microtasks and macrotasks in the event loop is their execution priority and execution time. Macrotask will wait for the synchronization code to complete execution before executing in the next event loop, while microtask will execute immediately after the synchronization code has completed execution, regardless of whether there are still macrotasks waiting to be executed. Therefore, microtask execution priority is higher than macrotask.

11. What is the relationship between Promise and microtask in JavaScript?

Promise and microtask are closely related. When the state of a Promise changes (that is, from pending to resolved or rejected), the execution of the microtask queue is triggered. This means that the Promise’s callback function (whether .then or .catch) will be added to the microtask queue and executed in the next event loop.

Therefore, we can use the relationship between Promise and microtask to ensure that the code executed after the asynchronous operation is completed can be executed immediately without waiting for the current synchronous code to complete execution. For example, if we perform an asynchronous operation in a Promise callback function, then the asynchronous operation will also be added to the microtask queue and executed in the next event loop. Therefore, we can use this feature to ensure that the code executed after the asynchronous operation is completed can be executed immediately without waiting for the current synchronous code to complete execution.

Case

1. Microtask: Promise callback function, macrotask: setTimeout callback function

console.log('start');
setTimeout(() => console.log('setTimeout'), 0);
Promise.resolve().then(() => console.log('Promise'));
console.log('end');

// start
//end
// Promise
// setTimeout

Parsing: First output start, and then register a callback function through the setTimeout method, which will be added to the macro task queue. Then a Promise instance is created and a callback function is registered through the then method. This callback function will be executed when the state of the Promise object changes. Then output end. Because the Promise callback function is a microtask, it will be added to the microtask queue and wait for execution. After the synchronization task of the main thread is completed, the JavaScript engine will first execute the tasks in the microtask queue and output Promise, and then execute the tasks in the macrotask queue and output setTimeout .

2. Microtask: process.nextTick callback function, macrotask: setImmediate callback function

console.log('start');
setImmediate(() => console.log('setImmediate'));
process.nextTick(() => console.log('process.nextTick'));
console.log('end');

// start
//end
// process.nextTick
// setImmediate

Analysis: In the Node.js environment, the process.nextTick callback function is at the front of the microtask queue and has a higher priority than the Promise callback function. The setImmediate callback function is ranked at the end of the macro task queue. So, the above code will output start first, then end. After the synchronization task of the main thread is completed, the JavaScript engine will first execute the task in the microtask queue and output process.nextTick, and then execute the task in the macrotask queue and output setImmediate strong>.

3. Microtask: Promise callback function, macrotask: requestAnimationFrame callback function

console.log('start');
requestAnimationFrame(() => console.log('requestAnimationFrame'));
Promise.resolve().then(() => console.log('Promise'));
console.log('end');

// start
//end
// Promise
// requestAnimationFrame

Parsing: First output start, and then register a callback function through the requestAnimationFrame method, which will be added to the macro task queue. Then a Promise instance is created and a callback function is registered through the then method. This callback function will be executed when the state of the Promise object changes. Then output end. Because the Promise callback function is a microtask, it will be added to the microtask queue and wait for execution. After the synchronization task of the main thread is completed, the JavaScript engine will first execute the tasks in the microtask queue and output Promise, and then execute the tasks in the macrotask queue and output requestAnimationFrame .

4. Microtask: Promise callback function, macrotask: XMLHttpRequest callback function

console.log('start');
const xhr = new XMLHttpRequest();
xhr.open('get','http://localhost:4000/get',true)
xhr.onload = () => console.log('XMLHttpRequest');
xhr.send();
Promise.resolve().then(() => console.log('Promise'));
console.log('end');

//start
//end
//Promise
//XMLHttpRequest

Parsing: First output start, then create an XMLHttpRequest object, and send a GET request through the open method and the send method. Then a callback function is registered through the onload method, which will be executed after the request is successful. Then a Promise instance is created and a callback function is registered through the then method. This callback function will be executed when the state of the Promise object changes. Then output end. Because the Promise callback function is a microtask, it will be added to the microtask queue and wait for execution. After the synchronization task of the main thread is completed, the JavaScript engine will first execute the tasks in the microtask queue, output Promise, and then wait for the callback function of the XMLHttpRequest object to be executed. When the request is successful, the JavaScript engine will execute the tasks in the macro task queue and output XMLHttpRequest.

5. Final case:

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')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')


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

Analysis:

1. Execute the entire code and execute the synchronization task for the first time: log (script start)

2. If the timer is an asynchronous task, it will not be executed first.

3. When async1() is encountered, execute print async1 start and then execute await. After printing async2, block async1 end and add it to the microtask list.

4. Jump out of the function and continue to execute downwards. When a new promise is encountered, it is executed. Print promise1 and then encounter .then, which is a micro task and added to the micro object list.

5. Execute the last line of code after jumping out and print script end. At this point, the first round of macro tasks is completed and micro tasks are started.

6. At this time, the microtask list prints the async1 end and promise2 microtasks in order. At this point, the microtask execution is completed.

7. Start a new round of event loop and start executing the next macro task, that is, the timer and print settimeout