Understand microtasks and macrotasks in JavaScript

avatar
Mofei Zhu

If a piece of JavaScript code contains setTimeout, almost everyone knows that the code will be delayed (asynchronously), but if the code contains setTimeout, await and Promise resolve at the same time, then can you still the right order of execution?

This is a front-end interview question on the Internet. The main knowledge point to be examined is the execution sequence of asynchronous async/await, setTimeout, Promise resolve, which is the microtaks and microtaks we are going to discuss here:

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("promise end");
});

console.log("script end");

If you want to fully answer this question, you need to understand JavaScript's synchronous queue and asynchronous queue, i.e. the subdivided microtasks and macrotasks in the asynchronous queue.

Synchronous Asynchronous

We know that synchronous tasks will be executed first during the running process of JavaScript. If asynchronous code is encountered, the asynchronous code will be placed in the asynchronous queue, and the code in the asynchronous queue will be executed after the execution of the synchronous code is completed. For example the following code:

console.log(1)
setTimeout(()=>{
    console.log(2)
}, 0)
console.log(3)
setTimeout(()=>{
    console.log(4)
}, 0)
console.log(5)

The code first executes console.log(1) on the first line. Continue to execute and encounter the first asynchronous code setTimeout, at which point JavaScript will place the fragment code in the asynchronous queue (macrotasks to be exact, which will be discussed later), and then continue to execute the synchronous code console .log(3), and then encounters a second setTimeout, which is also placed in the asynchronous queue and continues to execute the code console.log(5). When all the synchronous codes are executed, go back and check whether there are any unexecuted tasks in the asynchronous tasks. If so, execute them in order, which is the setTimeout code that prints 2 and 4 respectively.

So the result of executing the above code is:

1
3
4
undefined
2
4

Priority in async: microtasks & macrotasks

First look at the following code:

console.log(1)
setTimeout(() => {
  console.log(2)
}, 0);
new Promise(function(resolve) {
  console.log("3");
  resolve();
}).then(function() {
  console.log("4");
});
console.log(5)

You must know that the synchronous code console.log(1) and console.log(4) will be executed first, but for the two asynchronous Promise.then and setTimeout, who will execute first?

Here we want to introduce the concepts of microtasks and macrotasks. Both microtasks and macrotasks are asynchronous tasks, but the difference is: * microtasks tasks take precedence over macrotasks tasks, which means that when the synchronization task is completed, the tasks in microtasks will be executed first, and then the tasks in macrotasks will be executed. * The execution of microtasks will take precedence over UI tasks (in fact, UI tasks belong to macrotasks), which means that if you keep inserting tasks into microtasks, the page will stop responding. * Common microtasks are: process.nextTick, Promises (where the Promises constructor is synchronous), queueMicrotask, MutationObserver * Common macrotasks are: setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering

So for the code above:

  1. First execute the synchronous code console.log(1)
  2. Encounter setTimeout and put it in macrotasks
  3. Encountered Promise executes the synchronous constructor console.log("3"); resolve()(Promise's constructor is synchronously executed! Promise's constructor is synchronously executed! Promise's construction The function is executed synchronously!) and put then into microstaks
  4. Continue to execute the synchronous code console.log(5)
  5. When the synchronous code is executed, execute the task in microstaks, which is console.log("4") in Promise.then
  6. After the execution of microstaks, execute the task in macrostaks, namely console.log(2) of setTimeout

So the result is javascript 1 3 5 4 undefined 2

Execution order of async and await fragments in asynchrony

Let's move on to the example:

async function async1(){
    console.log(2)
    await async2()
    console.log(3)
}
async function async2(){
    console.log(4)
}

console.log(1)
async1()
console.log(5)
Promise.resolve().then(() => console.log(6))
console.log(7)

Hmmm, let me guess:

  • First is the sync code console.log(1)
  • Then the sync code in async1 console.log(2)
  • followed by the sync code in async2 console.log(4)

But how to deal with this await? What about the code behind await?

In fact, the async function returns a Promise object. When the function is executed, once it encounters await, it will execute the method after await first, and then put all the following code into microtasks and continue to execute Synchronous tasks, so here will put the following console.log(3) into microtasks after executing await async2() in async1.

Continue is: * Put console.log(3) into microtasks, jump out of async1 and continue to execute the synchronous code console.log(5) * When a Promise is encountered, synchronously execute the Promise's constructor, where the Promise constructor is empty. * Put the code in then of Promise into microtasks * Continue to execute synchronous code console.log(7) * After the synchronous code is executed, start executing the code in microtasks, the order is console.log(3) in async1, console.log(6) in Promise.then

So the result is: javascript 1 2 4 5 7 3 6

Well, the relevant concepts are explained, let's go back and look at the famous interview question:

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("promise end");
});

console.log("script end");

Here is the result!

  • First execute the synchronous code console.log("script start")
  • Encounter setTimeout and put it in the macrotasks macro task
  • Continue to execute console.log("async1 start") in synchronous code async1
  • Execute console.log("async2"); in async2 synchronously, and put the code after await into microtasks
  • Encounter Promise, execute its constructor synchronously console.log("promise1"); and put the then function after resmove into microtasks
  • Continue to synchronously execute the console.log("script end") in the last line, the synchronization task is completed, check whether there is a task in microtasks, and execute it
  • Execute the content after the first task async1 await in microtasks console.log("async1 end");
  • Execute the next task of microtasks, console.log("promise end"); in Promise resolve
  • After microtasks is executed, check whether there are tasks in macrotasks
  • Execute the setTimeout callback in macrotasks console.log("setTimeout")

So the result is:

script start
async1 start
async2
promise1
script end
async1 end
promise end
undefined
setTimeout