「今日頭條」前端面試題和思路解析

一篇文章和一道面試題

最近,有篇名爲 《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:歡迎關注我的公衆號 “超哥前端小棧”,交流更多的想法與技術。

圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章