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
- all
Synchronization task
All execute on the main thread, forming oneExecution stack
(The call stack
); - It exists outside of the main thread
Task queue
, includingMacro task queue
andMicrotask queue
, as soon as the asynchronous operation ends will go to the correspondingTask queue
Add a task (callback function) to - Simply perform stack clearing (on the main thread
Synchronization task
Performed),JS engine
It reads and executes firstMicrotask queue
All tasks in, clearMicrotask queue
Then read and executeMacro task queue
The task at the top; - The main thread repeats step 3, which is called an event loop
Event 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:
Promise
The constructor takes a function that needs to be executed immediately and is a synchronization task, so it prints 1 first;- The first one
Promise
When it’s done, it adds its first microtask to the queuethen
Method, 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 followsPromise
Constructor, output 3; - The second
Promise
When it’s done, it adds its first microtask to the queuethen
Method, at this time, there are no synchronization tasks to execute, equivalent to the micro task temporarily completed, that is, the firstPromise
thethen
Method completes, so the first one is followedPromise
The secondthen
Method is added to the microtask queue. - At this time
Microtask queue
It 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 secondPromise
One of the firstthen
After the method completes, the peer then adds a second microtask to the queuePromis
The second of ethen
Method, at this pointMicrotask queue
It 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.