preface

In the last article discussed the Promise principle analysis, I thought this time I finally understand the Promise, so on the Internet to check the Promise related interview questions, consolidate the knowledge, when see the online to the Promise execution order of the question….. 😂😂 Who am I? Where I am? What am I doing? Today I’m going to take a closer look at the Promise implementation process in conjunction with the event loop

JS event loop mechanism

To understand the Promise execution order, let’s review the JS event loop mechanism, as well as microtasks and macroTasks.

JS is single threaded

JavaScript is single-threaded, with at most one JS engine thread executing JavaScript code at any one time. So why is JavaScript single threaded? JavaScript was originally designed to manipulate the DOM and interact with users; If one of the user’s actions triggers two instructions (adding and deleting DOM), how will the browser display the two instructions if they are executed at the same time, assuming that two threads execute the two instructions respectively? So, in order to avoid complex concurrency issues,JS was decreed to be single-threaded from the very beginning.

While single-threading can avoid complex concurrency problems, it also introduces the problem of synchronous blocking. Single threading means that tasks need to be queued up for execution one by one, and the first one must be finished before the next one can be processed. When the current task is a high-time operation, the following task can only be in the state of dry waiting, but often these high-time operation is not due to the JS engine thread processing slow cause, such as I/O task, timer task, network request task….. The page is blocked due to external reasons and the browser is not responding. To solve this problem, the JavaScript language has designed an asynchronous mode: when JS performs one of the above time-consuming operations, it suspends the waiting time-consuming task, skips it, continues to execute the following task, and comes back to execute the pending task when the result is available.

Task queue

In this way, JS processing tasks are divided into two kinds, synchronous tasks and asynchronous tasks; A synchronous task is a task that is not suspended by the engine and queued for execution. Only after the previous task is completed, can the next task be executed directly on the main thread. Asynchronous tasks are those that are suspended by the engine and put into a task queue. In a browser rendering process, in addition to JS engine threads, there is an event thread, timer thread, such as HTTP request thread handle asynchronous tasks to synergy the main thread, the thread has a common effect, that is at the end of the asynchronous operation, add pending tasks into the task queue waiting for the main thread to execute. SetTimeout (() => {}) is an asynchronous task. When the JS engine thread reaches setTimeout, the timer thread is responsible for handling the timing. After the asynchronous task completes (timed termination), the timer thread adds the callback function to the task queue and waits for the JS engine thread to execute it.

Microtasks and macro tasks

Tasks created in asynchronous mode are divided into two types: microtasks and macro tasks. According to ES6 specification, microtasks are called Jobs and macrotasks are called Tasks. Microtasks are initiated by the JS itself, while macro tasks are initiated by the host (browser). In terms of priority of execution order, the priority of microtask is higher than that of macro task, that is, JS engine performs microtask first and then macro task. Common microtask and macro task in browser environment:

Macro task Micro tasks
setTimeout() window.queueMicrotask()
setInterval() Promise.then/catch/finally()
The event queue
Script overall code is fast

Why is writing a script as a whole faster a macro task? Just look at the following example

<script>
  console.log('script1 start')
  setTimeout(() = > console.log('setTimeout1'),0)
  queueMicrotask(() = > console.log('Microtask1'))
  console.log('script1 end')
</script>
<script>
  console.log('script2 start')
  setTimeout(() = > console.log('setTimeout2'),0)
  queueMicrotask(() = > console.log('Microtask2'))
  console.log('script2 end')
</script>
Copy the code

The console printing sequence is as follows: Script1 start, script1 end, Microtask1, script2 start, script2 end, Microtask2, setTimeout1, setTimeout2 After performing synchronization tasks in the first

Let’s review the difference between macro and micro tasks: defined in MDN:

  • When executing tasks from the task queue, each task in the queue is executed by the runtime at the beginning of each iteration of the new event loop. Tasks added to the queue at the beginning of each iteration will not be executed until the next iteration begins.
  • Each task in the microtask queue is executed in turn each time a task exits and the execution context is empty. The difference is that it waits until the microtask queue is empty before stopping execution — even if a microtask joins in mid-stream. In other words, a microtask can add new microtasks to the queue and complete all of the microtasks before the next task starts and the current event loop ends.

To sum up: Both are executed after the execution stack is cleared (the current synchronous code has completed execution). The main difference between the two is the difference in execution timing. If the microtask queue is not empty during the event cycle, all the microtasks in the microtask queue will be emptied first. Even if a new task is added to the microtask queue during the execution of the microtask, it must be emptied before the next event loop. In the task queue, only the first task in the queue is executed during an event cycle.

Usually we put these so-called tasks in the queue, so far we seem to encounter the form of callback function, let’s consider the

Event loop

  1. allSynchronization taskAll execute on the main thread, forming oneExecution stack(The call stack);
  2. It exists outside of the main threadTask queue, includingMacro task queueandMicrotask queue, as soon as the asynchronous operation ends will go to the correspondingTask queueAdd a task (callback function) to
  3. Simply perform stack clearing (on the main threadSynchronization taskPerformed),JS engineIt reads and executes firstMicrotask queueAll tasks in, clearMicrotask queueThen read and executeMacro task queueThe task at the top;
  4. The main thread repeats step 3, which is called an event loopEvent Loop

Promise execution order

With that in mind, it’s easier to understand the order in which promises are executed

setTimeout(() = >{
   console.log(1)})new Promise((resolve) = >{
    console.log(2)
    resolve()
}).then(() = >{
   console.log(3)})console.log(4) 
Copy the code

For the sake of naming, we’ll assume that the number printed in the code is the number of steps; Step 1 in the code is a macro task, step 2 is the first synchronization task performed on the main thread, step 3 is a microtask, and step 4 is also a synchronization task performed on the main thread. So the printing sequence is: 2, 4, 3, 1

new Promise((resolve, reject) = > {
    console.log("1");
    resolve();
  })
.then(() = > {
    console.log("2");
    new Promise((resolve, reject) = > {
        console.log("3");
        resolve();
    })
    .then(() = > {
        console.log("4");
    })
    .then(() = > {
        console.log("5");
    });
})
.then(() = > {
    console.log("6");
});
Copy the code

A little more complicated than the last one, step by step:

  1. PromiseThe constructor takes a function that needs to be executed immediately and is a synchronization task, so it prints 1 first;
  2. The first onePromiseWhen it’s done, it adds its first microtask to the queuethenMethod, because the stack is currently empty (there is no synchronous code to execute), executes the tasks in the microtask queue, printing 2 before executing the second task that followsPromiseConstructor, output 3;
  3. The secondPromiseWhen it’s done, it adds its first microtask to the queuethenMethod, at this time, there are no synchronization tasks to execute, equivalent to the micro task temporarily completed, that is, the firstPromisethethenMethod completes, so the first one is followedPromiseThe secondthenMethod is added to the microtask queue.
  4. At this timeMicrotask queueIt should look like this: [console.log("4").console.log("6")], the main thread executes in sequenceMicrotask queue, output 4 after the first microtask is completed, which also means the secondPromiseOne of the firstthenAfter the method completes, the peer then adds a second microtask to the queuePromisThe second of ethenMethod, at this pointMicrotask queueIt should look like this: [console.log("6").console.log("5")], the program continues in sequenceMicrotask queue, output 6 and 5 in turn, the program is completed

So the whole process is printed in sequence: 1, 2, 3, 4, 6, 5

new Promise((resolve, reject) = > {
    console.log("1");
    resolve();
  })
.then(() = > {
    console.log("2");
    return new Promise((resolve, reject) = > {
        console.log("3");
        resolve();
    })
    .then(() = > {
        console.log("4");
    })
    .then(() = > {
        console.log("5");
    });
})
.then(() = > {
    console.log("6");
});
Copy the code

This is similar to the previous example, except that in the first Promise then method there is a return. As we know from the Promise source code, the return value type of the function in the THEN method determines the internal state of the next THEN method. Therefore, if there is a return in the THEN method, we need to complete the expression after the return before calling the next THEN method. The second then method of the first Promise hangs on the return value. So the whole process is printed in sequence: 1, 2, 3, 4, 5, 6

conclusion

The argument received by the Promise constructor is a synchronous task that needs to be executed synchronously. The THEN method of the Promise instance is a micro-task, and the then method is invoked after the Promise constructor completes execution. If a new microtask is created during the execution of the microtask, it is directly added to the end of the microtask queue and executed before the end of the current event loop.