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('promise2');
});
console.log('script end');
/*
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
*/
This question mainly examines the order of function execution in the event loop, including async
, await
, setTimeout
, Promise
functions. Let's talk about the knowledge points involved in this question.
1. Task queue
First we need to understand the following things:
- JS is divided into synchronous tasks and asynchronous tasks
- Synchronous tasks are executed on the main thread, forming an execution stack
- In addition to the main thread, the event trigger thread manages a task queue. As long as the asynchronous task has a running result, an event is placed in the task queue.
- Once all synchronous tasks in the execution stack are executed (the JS engine is idle at this time), the system will read the task queue, add runnable asynchronous tasks to the executable stack, and start execution.
According to the specification, the event loop is coordinated through the mechanism of task queue . In an Event Loop, there can be one or more task queues (task queue), a task queue is a collection of ordered tasks (tasks); each task has a task source (task source), from the same Tasks from a task source must be put into the same task queue, and tasks from different sources are added to different queues. APIs such as setTimeout/Promise are task sources, and what enters the task queue is the specific execution task they specify.
macro task
(macro)task (also known as a macro task), it can be understood that the code executed by the execution stack each time is a macro task (including each time an event callback is obtained from the event queue and placed in the execution stack for execution).
In order to enable the orderly execution of JS internal (macro) tasks and DOM tasks, the browser will re-render the page after a (macro) task is executed and before the next (macro) task is executed. The process is as follows:
(macro)task->渲染->(macro)task->...
(macro) task mainly includes: script (overall code), setTimeout, setInterval, I/O, UI interaction events, postMessage, MessageChannel, setImmediate (Node.js environment)
micro task
Microtask (also known as microtask), can be understood as a task that is executed immediately after the execution of the current task ends . That is, after the current task, before the next task, and before rendering.
So its response speed will be faster than setTimeout (setTimeout is a task), because there is no need to wait for rendering. That is to say, after a macrotask is executed, all microtasks generated during its execution will be executed (before rendering).
Microtask mainly includes: Promise.then, MutaionObserver, process.nextTick (Node.js environment)
operating mechanism
In the event loop, each cycle operation is called tick, and the task processing model of each tick is relatively complicated, but the key steps are as follows:
- Execute a macro task (get it from the event queue if it is not in the stack)
- If a microtask is encountered during execution, it is added to the task queue of the microtask
- After the macro task is executed, immediately execute all micro tasks in the current micro task queue (executed sequentially)
- After the current macro task is executed, the rendering starts to be checked, and then the GUI thread takes over the rendering
- After rendering, the JS thread continues to take over and start the next macro task (obtained from the event queue)
The flow chart is as follows:
Immediate execution in Promise and async
We know that the asynchrony in Promise is reflected in then
and catch
, so the code written in Promise is executed immediately as a synchronous task. In async/await, the code in it is executed immediately before await appears. So what happened when await appeared?
what does await do
Literally, await is to wait. What await waits for is an expression. The return value of this expression can be a promise object or other values.
Many people think that await will wait until the subsequent expression is executed before continuing to execute the following code. In fact, await is a sign to give up the thread. The expression after await will be executed first, the code after await will be added to the microtask, and then the entire async function will be jumped out to execute the following code.
Because async await itself is the syntactic sugar of promise+generator. So the code behind await is microtask. So for this question
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); }
Equivalent to
async function async1() { console.log('async1 start'); Promise.resolve(async2()).then(() => { console.log('async1 end'); }) }
back to this topic
The above is all the relevant knowledge points involved in this question, let's go back to this question to see what's going on step by step.
-
First, the event loop starts from the macrotask queue. At this time, there is only one script (whole code) task in the macrotask queue; when a task source is encountered, the task will be distributed to the corresponding task first. go in the queue. Therefore, the first step of the above example is executed as shown in the following figure:
-
Then we saw that two async functions were defined first, then looked down, and then encountered
console
a statement and output it directlyscript start
. After the output, the script task continues to execute. When encounteringsetTimeout
it as a macro task source, it will first distribute its tasks to the corresponding queue: -
The script task continues to execute, and the async1() function is executed. As mentioned above, the code before await in the async function is executed immediately, so it will be output immediately
async1 start
.When encountering await, the expression after await will be executed once, so it will be output immediately
async2
, and then the code after await will beconsole.log('async1 end')
added to the Promise queue in microtask, and then the async1 function will be jumped out to execute the following code. -
The script task continues to execute and encounters a Promise instance. Since the functions in Promise are executed immediately, the subsequent
.then
ones will be distributed to the microtaskPromise
queue. So it will be output firstpromise1
, then executedresolve
, and willpromise2
be assigned to the corresponding queue. -
The script task continues to execute, and only one sentence is output at the end
script end
. At this point, the execution of the global task is completed.According to the above, after each execution of a macro task, it will check whether there are Microtasks; if so, execute Microtasks until the Microtask Queue is cleared.
Therefore, after the execution of the script task is completed, it starts to search and clear the microtask queue. At this time, in the microtask,
Promise
there are two tasks in the queueasync1 end
,promise2
so they are output in sequenceasync1 end,promise2
. When all Microtasks are executed, it means that the first round of the cycle is over. -
At the beginning of the second cycle, it will jump back to the async1 function to execute the following code, and then encounter a synchronous task
console
statement and output it directlyasync1 end
. This completes the second round of the cycle. (It can also be understood as being added to the script task queue, so it will be executed first with the setTimeout queue) -
The second cycle still starts from the macro task queue. At this time, there is only one macro task
setTimeout
, just take it out and output it directly, and the whole process ends here.
Variant one
In the first variant, I also turned the function in async2 into a Promise function, the code is as follows:
async function async1() { console.log('async1 start'); await async2(); console.log('async1 end'); } async function async2() { //async2 makes the following changes: new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); } console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0) async1(); new Promise(function(resolve) { console.log('promise3'); resolve(); }).then(function() { console.log('promise4'); }); console.log('script end');
You can first see for yourself what the output order will be, and the results will be announced below:
script start async1 start promise1 promise3 script end promise2 async1 end promise4 setTimeout
After the first macrotask is executed, that is, script end
after the output, all microtasks will be cleaned up. So it will output one after another promise2
, async1 end
, promise4
, and I won’t say more about the rest.
Variant two
In the second variant, I changed the code after await in async1 and the code of async2 to asynchronous, the code is as follows:
async function async1() { console.log('async1 start'); await async2(); //Change as follows: setTimeout(function() { console.log('setTimeout1') },0) } async function async2() { //Change as follows: setTimeout(function() { console.log('setTimeout2') },0) } console.log('script start'); setTimeout(function() { console.log('setTimeout3'); }, 0) async1(); new Promise(function(resolve) { console.log('promise1'); resolve(); }).then(function() { console.log('promise2'); }); console.log('script end');
You can first see for yourself what the output order will be, and the results will be announced below:
script start async1 start promise1 script end promise2 setTimeout3 setTimeout2 setTimeout1
After the output is promise2
, it will be output sequentially in the order of adding to the setTimeout queue. Through the code, we can see that the adding order is 3 2 1
, so it will be output in the order of 3, 2, 1.
Variant three
Variant 3 is the original question I saw in a face-to-face scripture. Overall, it is similar with minor differences. The code is as follows:
async function a1 () { console.log('a1 start') await a2() console.log('a1 end') } async function a2 () { console.log('a2') } console.log('script start') setTimeout(() => { console.log('setTimeout') }, 0) Promise.resolve().then(() => { console.log('promise1') }) a1() let promise2 = new Promise((resolve) => { resolve('promise2.then') console.log('promise2') }) promise2.then((res) => { console.log(res) Promise.resolve().then(() => { console.log('promise3') }) }) console.log('script end')
It’s nothing more than doing some articles on micro-tasks. If you understand the previous content, this question must be no problem. The results are as follows:
script start a1 start a2 promise2 script end promise1 a1 end promise2.then promise3 setTimeout