Thought leadership from the most innovative tech companies, all in one place.

6 Interview Questions That Combine Promise and setTimeout

Thoroughly master this type of interview question.

Photo by Bharat Patil on Unsplash

Photo by Bharat Patil on Unsplash

Before we start, I hope you can clear up a few knowledge points.

The event loop is executed in the following order:

  • There are two task queues in the JS engine: macrotask queue and microtask queue

  • The entire script is initially executed as a macro task

  • During the execution, the synchronization code executes directly, and the macrotask enters the macro task queue, and the microtask enters the microtask queue

  • When the current macro task is completed, the microtask queue is checked and all the microtasks are executed in turn

  • Performs rendering of the browser UI thread (You can ignore this in this article)

  • If any Web Worker task exists, execute it (You can ignore this in this article)

  • Check the macro task queue, and if it is not empty, go back to step 2 and execute the next macro task.

It is worth noting that step 4: when a macrotask is completed, all other microtasks are executed in turn first, and then the next macrotask is executed.

Mircotasks include: MutationObserver, Promise.then() and Promise.catch(), other techniques based on Promise such as the fetch API, V8 garbage collection process, process.nextTick() in node environment.

Marcotasks include: initial script, setTimeout, setInterval, setImmediate, I/O, UI rendering.

Well, it doesn't matter if you don't fully understand what's going on here, let's practice with examples.

There are 10 questions: the first 4 are simple Promise questions to help you understand microtasks; the next 6 questions are a mix of Promise and setTimeout.

1.

Let's start with a simple example to explain the microtask.

Example:

const promise1 = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
});

promise1.then(() => {
  console.log(3);
});

console.log(4);

Process Analysis:

  • First, the first four lines of this code are executed. The console will print out 1, and then promise1 will turn into a resolved state.

  • Then start executing the promise1.then(() => {console.log(3);}); snippet. Because promise1 is now in the resolved state, the () => {console.log(3);} will be added to the microtask queue immediately.

  • But we know () => {console.log(3);} is a microtask, so it is not immediately called.

  • Then the last line of code (console.log(4);) is executed and 4 is printed in the console.

  • At this point, all the synchronized code, the current macrotask, is executed. Then the JavaScript engine checks the queue of microtasks and executes them in turn.

  • () => {console.log(3);} is then executed and 4 is printed in the console.

Result:

image

2.

Example:

const promise1 = new Promise((resolve, reject) => {
  console.log(1);
});

promise1.then(() => {
  console.log(3);
});

console.log(4);

Process Analysis:

This example is very similar to the previous one, except that in this one, promise1 will always be in a pending state, so () => {console.log(3);} won't be executed and the console won't print 3.

Result:

image

3.

Example:

const promise1 = new Promise((resolve, reject) => {
  console.log(1)
  resolve('resolve1')
})

const promise2 = promise1.then(res => {
  console.log(res)
})

console.log('promise1:', promise1);
console.log('promise2:', promise2);

Consider carefully the order in which the console prints the results and the state of each promise.

Process Analysis:

  • First, the first four lines of code are the same as before, 1 is printed in the console, and the state of promise1 is resolved.

  • Then execute const promise2 = promise1.then(...), res => {console.log(res)} is added to the microtask queue. At the same time, promise1.then() will return a new pending promise object.

  • Then execute console.log(‘promise1:', promise1);, and the console prints out the string 'promise1' and the promise1 in the resolved state.

  • Then execute console.log(‘promise2:', promise2);, and the console prints out the string ‘promise2' and the promise2 in the pending state.

  • At this point, all the synchronized code, the current macrotask, is executed. Then the JavaScript engine checks the queue of microtasks and executes them in turn.

  • res => {console.log(res)} is the only task in the microtask queue, and it will be executed now. And then the console will print 'reslove1'.

Result:

image

4.

Example:

const fn = () => (new Promise((resolve, reject) => {
  console.log(1)
  resolve('success')
}));

fn().then(res => {
  console.log(res)
});

console.log(2)

Process Analysis:

Unlike before, in this example, the behavior of creating a Promise object occurs within the fn function. While the fn function is a normal synchronization function, there is nothing special about it, and this example is still simple.

Result:

image

The previous examples are relatively simple, now the questions will gradually become more complex, are you ready?

5.

Example:

console.log('start')

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

Promise.resolve().then(() => {
  console.log('resolve')
})

console.log('end')

Process Analysis:

  • First, there are two task queues in the JS engine: the macrotask queue and the microtask queue.

image

  • At the beginning of the program, all the initial code is treated as a macro task, pushed into the macro task queue.

image

  • The first line of code console.log('start') is then executed and ‘start' is printed in the console.

image

  • Then the setTimeout(...) is a timer with a wait time of 0, which will be executed immediately. As we mentioned at the beginning of this article, setTimeout is a macrotask, so the callback function of setTimeout(...), () => {console.log(‘setTimeout')}, will not be executed immediately, it will be pressed into the macro task queue, waiting to be executed later.

image

  • Then it start executing Promise.resolve().then(…), and () => {console.log('resolve')} gets pushed into the queue of microtasks.

image

  • Now execute console.log(‘end'), print ‘end' in the console, and the first macro task completes.

image

  • When a macro task completes, the JS engine checks the queue of microtasks first and executes all the microtasks in turn.

image

  • When the microtask queue is empty, the JS engine checks the macro task queue and starts executing the next macro task.

image

It is worth emphasizing that while setTimeout(...) is executed earlier than Promise.resolve().then(...), the callback function for setTimeout(...) is still executed later because setTimeout is a macro task. This is where the novice makes the most mistakes.

Okay, so that's how the sample code above runs. I hope my sketch will help you.

Result:

image

6.

Example:

const promise = new Promise((resolve, reject) => {
  console.log(1);
  setTimeout(() => {
    console.log("timerStart");
    resolve("success");
    console.log("timerEnd");
  }, 0);
  console.log(2);
});

promise.then((res) => {
  console.log(res);
});

console.log(4);

Process Analysis:

  • First of all, we temporarily ignore those callback functions and simplify the code:

    const promise = new Promise((resolve, reject) => {
      console.log(1);
      setTimeout(..., 0);
      console.log(2);
    });
    
    promise.then(...);
    
    console.log(4);
    
  • Then we draw the picture as before. At first all the code can be thought of as a macro task.

image

  • Then start executing new Promise(...), and then go inside the executor and executing console.log(1).

image

  • Then start executing setTimeout(..., 0). The timer finish immediately and its callback function was pushed into the macrotask queue.

image

  • Then start executing console.log(2).

image

  • Now start executing promise.then(…). Because the promise object is still in the pending state, its callback function is not yet pressed into the queue of microtasks. That is, the microtask queue is currently still empty.

image

  • Then start executing console.log(4).

image

  • At this point, the first macro task ends, and the microtask queue is still empty, so the JS engine starts the next macro task.

  • Then start executing console.log(‘timerStart').

image

  • Now that the resolve() function is executed, the state of the promise will be resolved and the callback function of promise.then(…) is pushed into the queue of microtasks.

image

  • Then start executing console.log(‘timerEnd').

image

  • Now that the current macro task is over, the JS engine checks the microtask queue again and executes them in turn.

image

Result:

image

7.

Example:

const timer1 = setTimeout(() => {
  console.log('timer1');

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

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

console.log('start')

Process Analysis:

There are three setTimeout function in this example, so the program accumulates three additional macro tasks.

  • First, let's draw the initial macro task queue.

image

  • Then start executing the setTimeout(...) corresponding to timer1. Meanwhile, a new macro task is created.

image

  • Then start executing the setTimeout corresponding to timer2. Meanwhile, another new macro task is created.

image

  • Okay, now we have three macro tasks, no microtasks.

  • Then

image

  • Now that the first macro task and its execution are complete, and the microtask queue is still empty, the JS engine will start executing the next macro task.

  • console.log('timer1') is executed.

image

  • Then start executing the setTimeout(...) corresponding to timer3. A new macro task is created.

image

  • Then

image

  • Then

image

Result:

image

8.

Example:

const timer1 = setTimeout(() => {
  console.log('timer1');
  const promise1 = Promise.resolve().then(() => {
    console.log('promise1')
  })
}, 0)

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

console.log('start')

Process Analysis:

This example is similar to the previous one, except that we replaced one of the setTimeout with a Promise.then. Because setTimeout is a macro task and Promise.then is a microtask, and microtasks take precedence over macro tasks, the order of the output from the console is not the same.

  • First, let's draw the initial task queues.

image

  • Then

image

  • Then

image

  • Then

image

  • Then

image

  • Then

image

  • Notice at this point that Promise.then() is creating a microtask. Its callback function is executed by the JS engine before the next macro task.

image

  • Then the end.

image

Result:

image

9.

Example:

const promise1 = Promise.resolve().then(() => {
  console.log('promise1');
  const timer2 = setTimeout(() => {
    console.log('timer2')
  }, 0)
});

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

console.log('start');

Process Analysis:

In this example, macro tasks and microtasks are created alternately, which is a difficult topic. If you're just thinking in your head, it's very easy to make a mistake. But if you start drawing diagrams with me, it's easy to find the right answer.

  • First, let's draw the initial macro task queue.

image

  • The first piece of code is then executed, and a microtask is created.

image

  • The second piece of code is then executed, and a macro task is created

image

  • Then

image

  • The current macro task completes and the task in the microtask queue begins.

image

  • Then, start executing setTimeout(...) relate to timer2 and create a new macro task

image

  • The current microtask queue is cleared and the next macro task is started.

image

  • Then, another microtask is created.

image

  • The current macro task is completed. The JS engine checks the microtask queue again, finds that the queue is not empty, and starts prioritizing the tasks in the microtask queue.

image

  • Finally

image

Result:

image

10.

Example:

const promise1 = new Promise((resolve, reject) => {
  const timer1 = setTimeout(() => {
    resolve('success')
  }, 1000)
})

const promise2 = promise1.then(() => {
  throw new Error('error!!!')
})

console.log('promise1', promise1)
console.log('promise2', promise2)

const timer2 = setTimeout(() => {
  console.log('promise1', promise1);
  console.log('promise2', promise2);
}, 2000)

Process Analysis:

  • First, it created promise1 through new Promise(…), which is in the pending state. A timer with a delay of 1 second is also created.

  • Then execute const promise2 = promise1.then(...), because promise1 is currently in a Pending state, so the callback function of promise1.then() won't be added to the microtask queue yet.

  • Then execute console.log(‘promise1', promise1). At this point, the promise1 is still Pending.

  • Then execute console.log(‘promise2', promise2). At this point, the promise2 is still Pending.

  • Then execute const timer2 = setTimeout(…). A timer with a delay of 2 seconds is also created.

  • After 1000 milliseconds, timer1 was completed. Then resolve(‘success') is then executed, and promise1 becomes resolved.

  • promise1.then(...)'s callback function is called, and throw new Error(‘error!!!') was executed. An error was thrown and promise2 became reject.

  • After another 1000 milliseconds, timer2 was completed. () => {console.log(‘promise1', promise1); console.log(‘promise2', promise2);} is executed.

Result:

image

That's it for this topic. Thank you for reading.




Continue Learning