一篇文章和一道面試題
最近,有篇名為 《8張圖幫你一步步看清 async/await 和 promise 的執行順序》 的文章引起了我的關注。
作者用一道2017年「今日頭條」的前端面試題為引子,分步講解了最終結果的執行原因。其中涉及到了不少概念,比如非同步的執行順序,巨集任務,微任務等等,同時作者限定了執行範圍,以瀏覽器的 event loop 機制為準。下面是原題的程式碼:
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
promise2
async1 end
setTimeout
複製程式碼
我在看這道題的時候,先按照自己的理解寫出了結果。
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
複製程式碼
一些重要的概念
這裡需要先簡單地說一些 event loop 的概念。
- Javascript是單執行緒的,所有的同步任務都會在主執行緒中執行。
- 主執行緒之外,還有一個任務佇列。每當一個非同步任務有結果了,就往任務佇列裡塞一個事件。
- 當主執行緒中的任務,都執行完之後,系統會 “依次” 讀取任務佇列裡的事件。與之相對應的非同步任務進入主執行緒,開始執行。
- 非同步任務之間,會存在差異,所以它們執行的優先順序也會有區別。大致分為 微任務(micro task,如:Promise、MutaionObserver等)和巨集任務(macro task,如:setTimeout、setInterval、I/O等)。同一次事件迴圈中,微任務永遠在巨集任務之前執行。
- 主執行緒會不斷重複上面的步驟,直到執行完所有任務。
另外,還有 async/await 的概念。
- async 函式,可以理解為是Generator 函式的語法糖。
- 它建立在promise之上,總是與await一起使用的。
- await會返回一個Promise 物件,或者一個表示式的值。
- 其目的是為了讓非同步操作更優雅,能像同步一樣地書寫。
我的理解
再說說我對這道題的理解。
- 首先,從console的數量上看,會輸出8行結果。
- 再瞟了一眼程式碼,看到了setTimeout,於是,默默地把它填入第8行。
- 在setTimeout附近,看到了 console.log( 'script start' ) 和 async1(),可以確認它們是同步任務,會先在主執行緒中執行。所以,妥妥地在第1行填入 script start,第2行填入async1方法中的第一行 async1 start。
- 接下來,遇到了await。從字面意思理解,讓我們等等。需要等待async2()函式的返回,同時會阻塞後面的程式碼。所以,第3行填入 async2。
- 講道理,await都執行完了,該輪到console.log( 'async1 end' )的輸出了。但是,別忘了下面還有個Promise,有一點需要注意的是:當 new 一個 Promise的時候,其 resolve 方法中的程式碼會立即執行。如果不是 async1()的 await 橫插一槓,promise1 可以排得更前面。所以,現在第4行填入 promise1。
- 再接下來,同步任務 console.log( 'script end' ) 執行。第5行填入 script end。
- 還有第6和第7行,未填。回顧一下上面提到 async/await 的概念,其目的是為了讓非同步能像同步一樣地書寫。那麼,我認為 console.log( 'async1 end' ) 就是個同步任務。所以,第6行填入async1 end。
- 最後,順理成章地在第7行填入 promise2。
與作者答案的不同
回過頭對比與作者的答案,發現第6和第7行的順序有問題。
再耐心地往下看文章,反覆地看了幾遍 async1 end 和 promise2 誰先誰後,還是無法理解為何在chrome瀏覽器中,promise2 會先於 async1 end 輸出。
然後,看到評論區,發現也有人提出了相同的疑惑。@rhinel提出,在他的72.0.3622.0(正式版本)dev(64 位)的chrome中,跑出來的結果是 async1 end 在 promise2 之前。
隨即我想到了一種可能,JS的規範可能會在未來有變化。於是,我用自己的react工程試了一下(工程中的babel-loader版本為7.1.5。.babelrc的presets設定了stage-3),結果與我的理解一致。當前的最新版本 chromeV71,在這裡的執行順序上,的確存在有問題。
於是,我也在評論區給作者留了言,進行了討論。@rhinel最後也證實,其實最近才釋出通過了這個順序的改進方案,這篇 《Faster async functions and promises》 詳細解釋了這個改進,以及實現效果。不久之後,作者也在他文章的最後,補充了我們討論的結果,供讀者參考。
總結
最後,我想說的是,本文雖然只是由一道面試題引申出的,對瀏覽器執行順序的思考、討論與驗證的過程。但正是因為有了這些過程,才讓更多的思想得以碰撞,概念進一步得以理解,規範得以明瞭。
有機會的話,希望能有與更多的同道,多多交流。
PS:歡迎關注我的公眾號 “超哥前端小棧”,交流更多的想法與技術。