Fundebug經授權轉載,版權歸原作者所有。
為什麼寫這篇文章?
說實話,關於js的非同步執行順序,巨集任務、微任務這些,或者async/await這些慨念已經有非常多的文章寫了。
但是怎麼說呢,簡單來說,業務中很少用async,不太懂async呢。
研究了一天,感覺懂了,所手癢想寫一篇 ,哈哈。
畢竟自己學會的知識,如果連表達清楚都做不到,怎麼能指望自己用好它呢?
測試一下自己有沒有必要看
所以我寫這個的文章,主要還是交流學習,如果您已經清楚了eventloop/async/await/promise這些東西呢,可以 break 啦
有說的不對的地方,歡迎留言討論,
那麼還是先通過一道題自我檢測一下,是否有必要繼續看下去把。
其實呢,這是去年一道爛大街的「今日頭條」的面試題。
我覺得這道題的關鍵,不僅是說出正確的列印順序,更重要的能否說清楚每一個步驟,為什麼這樣執行。
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");
複製程式碼
注:因為是一道前端面試題,所以答案是以瀏覽器的eventloop機制為準的,在node平臺上執行會有差異。
script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
複製程式碼
如果你發現執行結果跟自己想的一樣,可以選擇跳過這篇文章啦,
或者如果你有興趣看看俺倆的理解有沒有區別,可以跳到後面的 「畫圖講解的部分」
需要具備的前置知識
- promise的使用經驗
- 瀏覽器端的eventloop
不過如果是對 ES7 的 async 不太熟悉,是沒關係的哈,因為這篇文章會詳解 async。
那麼如果不具備這些知識呢,推薦幾篇我覺得講得比較清楚的文章
- 《10分鐘理解JS引擎的執行機制》:這是我之前寫的講解eventloop的文章,我覺得還算清晰,但是沒涉及 async
- 《理解 JavaScript 的 async/await》:這是我讀過的講async await最清楚的文章
- 《ECMAScript 6 入門 - Promise 物件》:promise就推薦阮一峰老師的ES6吧,不過不熟悉 promise 的應該較少啦。
主要內容
第1部分:對於async await的理解
我推薦的那篇文章,對 async/await 講得更詳細。不過我希望自己能更加精煉的幫你理解它們這部分,主要會講解 3 點內容
- async 做一件什麼事情?
- await 在等什麼?
- await 等到之後,做了一件什麼事情?
- async/await 比 promise有哪些優勢?(回頭補充)
async 做一件什麼事情?
帶 async 關鍵字的函式,它使得你的函式的返回值必定是 promise 物件
也就是
如果async關鍵字函式返回的不是promise,會自動用Promise.resolve()包裝
如果async關鍵字函式顯式地返回promise,那就以你返回的promise為準
這是一個簡單的例子,可以看到 async 關鍵字函式和普通函式的返回值的區別
async function fn1(){
return 123
}
function fn2(){
return 123
}
console.log(fn1())
console.log(fn2())
Promise {<resolved>: 123}
123
複製程式碼
所以你看,async 函式也沒啥了不起的,以後看到帶有 async 關鍵字的函式也不用慌張,你就想它無非就是把return值包裝了一下,其他就跟普通函式一樣。
關於async關鍵字還有那些要注意的?
- 在語義上要理解,async表示函式內部有非同步操作
- 另外注意,一般 await 關鍵字要在 async 關鍵字函式的內部,await 寫在外面會報錯。
await 在等什麼?
await等的是右側「表示式」的結果
也就是說,
右側如果是函式,那麼函式的return值就是「表示式的結果」
右側如果是一個 'hello' 或者什麼值,那表示式的結果就是 'hello'
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
async1()
console.log( 'script start' )
複製程式碼
這裡注意一點,可能大家都知道await會讓出執行緒,阻塞後面的程式碼,那麼上面例子中, 'async2' 和 'script start' 誰先列印呢?
是從左向右執行,一旦碰到await直接跳出, 阻塞async2()的執行?
還是從右向左,先執行async2後,發現有await關鍵字,於是讓出執行緒,阻塞程式碼呢?
實踐的結論是,從右向左的。先列印async2,後列印的script start
之所以提一嘴,是因為我經常看到這樣的說法,「一旦遇到await就立刻讓出執行緒,阻塞後面的程式碼」
這樣的說法,會讓我誤以為,await後面那個函式, async2()也直接被阻塞呢。
await 等到之後,做了一件什麼事情?
那麼右側表示式的結果,就是await要等的東西。
等到之後,對於await來說,分2個情況
- 不是promise物件
- 是promise物件
如果不是 promise , await會阻塞後面的程式碼,先執行async外面的同步程式碼,同步程式碼執行完,再回到async內部,把這個非promise的東西,作為 await表示式的結果
如果它等到的是一個 promise 物件,await 也會暫停async後面的程式碼,先執行async外面的同步程式碼,等著 Promise 物件 fulfilled,然後把 resolve 的引數作為 await 表示式的運算結果。
第2部分:畫圖一步步看清巨集任務、微任務的執行過程
我們以開篇的經典面試題為例,分析這個例子中的巨集任務和微任務。
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");
複製程式碼
先分享一個我個人理解的巨集任務和微任務的慨念,在我腦海中巨集任務和為微任務如圖所示
也就是「巨集任務」、「微任務」都是佇列。
一段程式碼執行時,會先執行巨集任務中的同步程式碼,
- 如果執行中遇到setTimeout之類巨集任務,那麼就把這個setTimeout內部的函式推入「巨集任務的佇列」中,下一輪巨集任務執行時呼叫。
- 如果執行中遇到promise.then()之類的微任務,就會推入到「當前巨集任務的微任務佇列」中,在本輪巨集任務的同步程式碼執行都完成後,依次執行所有的微任務1、2、3
下面就以面試題為例子,分析這段程式碼的執行順序。
每次巨集任務和微任務發生變化,我都會畫一個圖來表示他們的變化。
直接列印同步程式碼 console.log('script start')
// 首先是2個函式宣告,雖然有async關鍵字,但不是呼叫我們就不看。然後首先是列印同步程式碼
console.log('script start')
複製程式碼
將setTimeout放入巨集任務佇列
預設所包裹的程式碼,其實可以理解為是第一個巨集任務,所以這裡是巨集任務2
呼叫async1,列印 同步程式碼 console.log( 'async1 start' )
我們說過看到帶有async關鍵字的函式,不用害怕,它的僅僅是把return值包裝成了promise,其他並沒有什麼不同的地方。所以就很普通的列印 console.log( 'async1 start' )
分析一下 await async2()
前文提過await,1.它先計算出右側的結果,2.然後看到await後,中斷async函式
- 先得到await右側表示式的結果。執行async2(),列印同步程式碼console.log('async2'), 並且return Promise.resolve(undefined)
- await後,中斷async函式,先執行async外的同步程式碼
目前就直接列印 console.log('async2')
被阻塞後,要執行async之外的程式碼
執行new Promise(),Promise建構函式是直接呼叫的同步程式碼,所以 console.log( 'promise1' )
程式碼執行到promise.then()
程式碼執行到promise.then(),發現這個是微任務,所以暫時不列印,只是推入當前巨集任務的微任務佇列中。
注意:這裡只是把promise2推入微任務佇列,並沒有執行。微任務會在當前巨集任務的同步程式碼執行完畢,才會依次執行
列印同步程式碼 console.log('script end')
沒什麼好說的。執行完這個同步程式碼後,「async外的程式碼」終於走了一遍
下面該回到 await 表示式那裡,執行await Promise.resolve(undefined)了
回到async內部,執行await Promise.resolve(undefined)
這部分可能不太好理解,我儘量表達我的想法。
對於 await Promise.resolve(undefined) 如何理解呢?
根據 MDN 原話我們知道
如果一個 Promise 被傳遞給一個 await 操作符,await 將等待 Promise 正常處理完成並返回其處理結果。
在我們這個例子中,就是Promise.resolve(undefined)正常處理完成,並返回其處理結果。那麼await async2()就算是執行結束了。
目前這個promise的狀態是fulfilled,等其處理結果返回就可以執行await下面的程式碼了。
那何時能拿到處理結果呢?
回憶平時我們用promise,呼叫resolve後,何時能拿到處理結果?是不是需要在then的第一個引數裡,才能拿到結果。
(呼叫resolve時,會把then的引數推入微任務佇列,等主執行緒空閒時,再呼叫它)
所以這裡的 await Promise.resolve() 就類似於
Promise.resolve(undefined).then((undefined) => {
})
複製程式碼
把then的第一個回撥引數 (undefined) => {} 推入微任務佇列。
then執行完,才是await async2()執行結束。
await async2()執行結束,才能繼續執行後面的程式碼
如圖
此時當前巨集任務1都執行完了,要處理微任務佇列裡的程式碼。
微任務佇列,先進選出的原則,
- 執行微任務1,列印promise2
- 執行微任務2,沒什麼內容..
但是微任務2執行後,await async2()語句結束,後面的程式碼不再被阻塞,所以列印
console.log('async1 end')
巨集任務1執行完成後,執行巨集任務2
巨集任務2的執行比較簡單,就是列印
console.log('setTimeout')
補充在不同瀏覽器上的測試結果
谷歌瀏覽器,目前是版本是「版本 71.0.3578.80(正式版本) (64 位)」 Mac作業系統
Safari瀏覽器的測試結果
火狐瀏覽器的測試結果
如果不理解可以留言,有錯誤的話也歡迎指正。