一、是什麼#
首先,JavaScript
是一門單線程的語言,意味著同一時間內只能做一件事,但是這並不意味著單線程就是阻塞,而實現單線程非阻塞的方法就是事件循環
在JavaScript
中,所有的任務都可以分為
-
同步任務:立即執行的任務,同步任務一般會直接進入到主線程中執行
-
異步任務:異步執行的任務,比如
ajax
網絡請求,setTimeout
定時函數等
同步任務與異步任務的運行流程圖如下:
從上面我們可以看到,同步任務進入主線程,即主執行棧,異步任務進入任務隊列,主線程內的任務執行完畢為空,會去任務隊列讀取對應的任務,推入主線程執行。上述過程的不斷重複就事件循環
二、宏任務與微任務#
如果將任務劃分為同步任務和異步任務並不是那麼的準確,舉個例子:
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
如果按照上面流程圖來分析代碼,我們會得到下面的執行步驟:
console.log(1)
,同步任務,主線程中執行setTimeout()
,異步任務,放到Event Table
,0 毫秒後console.log(2)
回調推入Event Queue
中new Promise
,同步任務,主線程直接執行.then
,異步任務,放到Event Table
console.log(3)
,同步任務,主線程執行
所以按照分析,它的結果應該是 1
=> 'new Promise'
=> 3
=> 2
=> 'then'
但是實際結果是:1
=>'new Promise'
=> 3
=> 'then'
=> 2
出現分歧的原因在於異步任務執行順序,事件隊列其實是一個 “先進先出” 的數據結構,排在前面的事件會優先被主線程讀取
例子中 setTimeout
回調事件是先進入隊列中的,按理說應該先於 .then
中的執行,但是結果卻偏偏相反
原因在於異步任務還可以細分為微任務與宏任務
微任務#
一個需要異步執行的函數,執行時機是在主函數執行結束之後、當前宏任務結束之前
常見的微任務有:
-
Promise.then
-
MutaionObserver
-
Object.observe(已廢棄;Proxy 對象替代)
-
process.nextTick(Node.js)
宏任務#
宏任務的時間粒度比較大,執行的時間間隔是不能精確控制的,對一些高實時性的需求就不太符合
常見的宏任務有:
- script (可以理解為外層同步代碼)
- setTimeout/setInterval
- UI rendering/UI 事件
- postMessage、MessageChannel
- setImmediate、I/O(Node.js)
這時候,事件循環,宏任務,微任務的關係如圖所示
按照這個流程,它的執行機制是:
- 執行一個宏任務,如果遇到微任務就將它放到微任務的事件隊列中
- 當前宏任務執行完成後,會查看微任務的事件隊列,然後將裡面的所有微任務依次執行完
回到上面的題目
console.log(1)
setTimeout(()=>{
console.log(2)
}, 0)
new Promise((resolve, reject)=>{
console.log('new Promise')
resolve()
}).then(()=>{
console.log('then')
})
console.log(3)
流程如下
// 遇到 console.log(1) ,直接打印 1
// 遇到定時器,屬於新的宏任務,留著後面執行
// 遇到 new Promise,這個是直接執行的,打印 'new Promise'
// .then 屬於微任務,放入微任務隊列,後面再執行
// 遇到 console.log(3) 直接打印 3
// 好了本輪宏任務執行完畢,現在去微任務列表查看是否有微任務,發現 .then 的回調,執行它,打印 'then'
// 當一次宏任務執行完,再去執行新的宏任務,這裡就剩一個定時器的宏任務了,執行它,打印 2
三、async 與 await#
async
是異步的意思,await
則可以理解為 async wait
。所以可以理解async
就是用來聲明一個異步方法,而 await
是用來等待異步方法執行
async#
async
函數返回一個promise
對象,下面兩種方法是等效的
function f() {
return Promise.resolve('TEST');
}
// asyncF is equivalent to f!
async function asyncF() {
return 'TEST';
}
await#
正常情況下,await
命令後面是一個 Promise
對象,返回該對象的結果。如果不是 Promise
對象,就直接返回對應的值
async function f(){
// 等同於
// return 123
return await 123
}
f().then(v => console.log(v)) // 123
不管await
後面跟著的是什麼,await
都會阻塞後面的代碼
async function fn1 (){
console.log(1)
await fn2()
console.log(2) // 阻塞
}
async function fn2 (){
console.log('fn2')
}
fn1()
console.log(3)
上面的例子中,await
會阻塞下面的代碼(即加入微任務隊列),先執行 async
外面的同步代碼,同步代碼執行完,再回到 async
函數中,再執行之前阻塞的代碼
所以上述輸出結果為:1
,fn2
,3
,2
四、流程分析#
通過對上面的了解,我們對JavaScript
對各種場景的執行順序有了大致的了解
這裡直接上代碼:
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')
分析過程:
- 執行整段代碼,遇到
console.log('script start')
直接打印結果,輸出script start
- 遇到定時器了,它是宏任務,先放著不執行
- 遇到
async1()
,執行async1
函數,先打印async1 start
,下面遇到await
怎麼辦?先執行async2
,打印async2
,然後阻塞下面代碼(即加入微任務列表),跳出去執行同步代碼 - 跳到
new Promise
這裡,直接執行,打印promise1
,下面遇到.then()
,它是微任務,放到微任務列表等待執行 - 最後一行直接打印
script end
,現在同步代碼執行完了,開始執行微任務,即await
下面的代碼,打印async1 end
- 繼續執行下一個微任務,即執行
then
的回調,打印promise2
- 上一個宏任務所有事都做完了,開始下一個宏任務,就是定時器,打印
settimeout
所以最後的結果是:script start
、async1 start
、async2
、promise1
、script end
、async1 end
、promise2
、settimeout