1. What is it#
First, JavaScript
is a single-threaded language, which means that only one thing can be done at a time. However, this does not mean that single-threading is blocking. The method to achieve single-threaded non-blocking is through the event loop.
In JavaScript
, all tasks can be divided into:
-
Synchronous tasks: tasks that are executed immediately. Synchronous tasks generally go directly to the main thread for execution.
-
Asynchronous tasks: tasks that are executed asynchronously, such as
ajax
network requests,setTimeout
timing functions, etc.
The flowchart of synchronous tasks and asynchronous tasks is as follows:
From the above, we can see that synchronous tasks enter the main thread, which is the main execution stack, and asynchronous tasks enter the task queue. When the tasks in the main thread are executed and empty, the corresponding tasks in the task queue will be read and pushed into the main thread for execution. This process is repeated continuously, which is the event loop.
2. Macro Tasks and Micro Tasks#
If tasks are divided into synchronous tasks and asynchronous tasks, it is not so accurate. Let's take an example:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
If we analyze the code according to the flowchart above, we will get the following execution steps:
console.log(1)
: synchronous task, executed in the main threadsetTimeout()
: asynchronous task, put into theEvent Table
, the callbackconsole.log(2)
is pushed into theEvent Queue
after 0 millisecondsnew Promise
: synchronous task, executed directly in the main thread.then
: asynchronous task, put into theEvent Table
console.log(3)
: synchronous task, executed in the main thread
So according to the analysis, the result should be 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
But the actual result is: 1
=> 'new Promise'
=> 3
=> 'then'
=> 2
The reason for the discrepancy lies in the execution order of asynchronous tasks. The event queue is actually a "first in, first out" data structure, and events that are in front will be read by the main thread first.
In the example, the callback event of setTimeout
enters the queue first and should be executed before the one in .then
, but the result is the opposite.
The reason is that asynchronous tasks can be further divided into micro tasks and macro tasks.
Micro Tasks#
A function that needs to be executed asynchronously, and its execution time is after the main function is executed and before the current macro task is completed.
Common micro tasks include:
-
Promise.then
-
MutationObserver
-
Object.observe (deprecated; replaced by Proxy objects)
-
process.nextTick (Node.js)
Macro Tasks#
Macro tasks have a larger time granularity and the execution interval cannot be precisely controlled, which is not suitable for some high real-time requirements.
Common macro tasks include:
- script (can be understood as outer synchronous code)
- setTimeout/setInterval
- UI rendering/UI events
- postMessage, MessageChannel
- setImmediate, I/O (Node.js)
At this time, the relationship between the event loop, macro tasks, and micro tasks is shown in the following diagram:
According to this process, the execution mechanism is as follows:
- Execute a macro task. If there are micro tasks encountered, put them into the micro task event queue.
- After the current macro task is completed, check the micro task event queue, and then execute all the micro tasks inside.
Going back to the previous question:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
The flow is as follows:
// Encountered console.log(1), directly print 1
// Encountered the timer, which is a new macro task, leave it for later execution
// Encountered new Promise, this is executed directly, print 'new Promise'
// .then is a micro task, put it into the micro task queue, execute it later
// Encountered console.log(3), directly print 3
// Okay, this round of macro task is completed, now check if there are any micro tasks in the micro task list. Found the callback of .then, execute it, print 'then'
// When a macro task is completed, go to execute a new macro task. Now there is only one timer macro task left, execute it, print 2
3. async and await#
async
means asynchronous, and await
can be understood as async wait
. So async
is used to declare an asynchronous method, and await
is used to wait for the execution of an asynchronous method.
async#
The async
function returns a promise
object. The following two methods are equivalent:
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await#
Under normal circumstances, the await
command is followed by a Promise
object, and it returns the result of the object. If it is not a Promise
object, it directly returns the corresponding value.
async function f(){
// Equivalent to
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
No matter what follows await
, it will block the code below.
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // Blocked
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
In the example above, await
will block the code below (i.e., add it to the micro task queue), execute the synchronous code outside the async
function first, and then go back to the async
function to execute the blocked code.
So the output of the above example is: 1
, fn2
, 3
, 2
4. Flow Analysis#
Through the above understanding, we have a general understanding of the execution order of JavaScript
in various scenarios.
Here is the code directly:
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')
})
async1()
new Promise(function (resolve) {
console.log('promise1')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
Analysis process:
- Execute the entire code. When encountering
console.log('script start')
, directly print the result, outputscript start
. - Encounter a timer, which is a macro task, so it is not executed first.
- Encounter
async1()
, execute theasync1
function, first printasync1 start
, then encounterawait
, what to do? Executeasync2()
, printasync2
, and then block the code below (i.e., add it to the micro task list), jump out to execute the synchronous code. - Jump to
new Promise
, execute it directly, printpromise1
, then encounter.then()
, which is a micro task, put it into the micro task list and wait for execution. - The last line directly prints
script end
, now the synchronous code is executed, start executing the micro tasks, that is, the code belowawait
, printasync1 end
. - Continue to execute the next micro task, that is, execute the callback of
.then()
, printpromise2
. - All the tasks of the previous macro task are completed, start the next macro task, which is the timer, print
settimeout
.
So the final result is: script start
, async1 start
, async2
, promise1
, script end
, async1 end
, promise2
, settimeout
.