故事還是要從下面這道面試題說起:請問下面這段代碼的輸出是什麼?
console.log("script start");
async function async1() {
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2 end");
}
async1();
setTimeout(function() {
console.log("setTimeout");
}, 0);
new Promise(resolve => {
console.log("Promise");
resolve();
})
.then(function() {
console.log("promise1");
})
.then(function() {
console.log("promise2");
});
console.log("script end");
複制
題目和答案
上述代碼中,在
Chrome 66
和
node v10
中,正确輸出是:
script start
async2 end
Promise
script end
promise1
promise2
async1 end
setTimeout
複制
注意:在新版本的浏覽器中,
await
輸出順序被“提前”了,請看官耐心慢慢看。
流程解釋
邊看輸出結果,邊做解釋吧:
- 正常輸出
script start
- 執行
函數,此函數中又調用了async1
函數,輸出async2
。回到async2 end
函數,遇到了async1
,讓出線程。await
- 遇到
,扔到下一輪宏任務隊列setTimeout
- 遇到
對象,立即執行其函數,輸出Promise
。其後的Promise
,被扔到了微任務隊列resolve
- 正常輸出
script end
- 此時,此次
宏任務都執行完了。來看下第二步被扔進來的微任務,因為Event Loop
函數是async2
關鍵詞修飾,是以,将async
後的代碼扔到微任務隊列中await async2
- 執行第 4 步被扔到微任務隊列的任務,輸出
和promise1
promise2
- 執行第 6 步被扔到微任務隊列的任務,輸出
async1 end
- 第一輪 EventLoop 完成,執行第二輪 EventLoop。執行
中的回調函數,輸出setTimeout
。setTimeout
再談 async 和 await
細心的朋友肯定會發現前面第 6 步,如果
async2
函數是沒有
async
關鍵詞修飾的一個普通函數呢?
// 新的async2函數
function async2() {
console.log("async2 end");
}
複制
輸出結果如下所示:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
複制
不同的結果就出現在前面所說的第 6 步:如果 await 函數後面的函數是普通函數,那麼其後的微任務就正常執行;否則,會将其再放入微任務隊列。
其實是 V8 引擎的 BUG
看到前面,正常人都會覺得真奇怪!(但是按照上面的訣竅倒也是可以了解)
然而 V8 團隊确定了這是個 bug(很多強行解釋要被打臉了),具體的 PR請看這裡。好在,這個問題已經在最新的 Chrome 浏覽器中被修複了。
簡單點說,前面兩段不同代碼的運作結果都是:
script start
async2 end
Promise
script end
async1 end
promise1
promise2
setTimeout
複制
await
就是讓出線程,其後的代碼放入微任務隊列(不會再多一次放入的過程),就這麼簡單了。