原文地址: https://lvdingjin.github.io/tech/2018/05/27/async-and-await.html
故事要從一道今日頭條的筆試題提及~
題目來源:半年工做經驗今日頭條和美團面試題面經分享!!!!!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') },0) async1(); new Promise(function(resolve){ console.log('promise1') resolve(); }).then(function(){ console.log('promise2') }) console.log('script end')
求打印結果是什麼?html
相信是個前端都知道啦,這道題目考的就是js裏面的事件循環和回調隊列咯~
今天題主假設看客都已經瞭解了setTimeout是宏任務會在最後執行的前提(由於它不是今天要討論的重點),咱們主要來說講promise、async和await之間的關係。前端
先上正確答案:java
script start async1 start async2 promise1 script end promise2 async1 end setTimeout
事實上,沒有在控制檯執行打印以前,我以爲它應該是這樣輸出的:git
script start async1 start async2 async1 end promise1 script end promise2 setTimeout
爲何這樣認爲呢?由於咱們(粗淺地)知道await以後的語句會等await表達式中的函數執行完獲得結果後,纔會繼續執行。github
MDN是這樣描述await的:面試
async 函數中可能會有 await 表達式,這會使 async 函數暫停執行,等待表達式中的 Promise 解析完成後繼續執行 async 函數並返回解決結果。
會認爲輸出結果是以上的樣子,是由於沒有真正理解這句話的含義。express
阮一峯老師的解釋我以爲更容易理解:segmentfault
async 函數返回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的異步操做完成,再接着執行函數體內後面的語句。
對啦就是這樣,MDN描述的暫停執行,其實是讓出了線程(跳出async函數體)而後繼續執行後面的腳本的。這樣一來咱們就明白了,因此咱們再看看上面那道題,按照這樣描述那麼他的輸出結果就應該是:promise
script start async1 start async2 promise1 script end async1 end promise2 setTimeout
好像哪裏不太對?對比控制檯輸出的正確結果,咦~有兩句輸出是不同的呀!!
async1 end promise2
爲何會這樣呢?這也是這道題目最難理解的一個地方。要搞明白這個事情,咱們須要先來回顧一些概念:
async function 聲明將定義一個返回 AsyncFunction 對象的異步函數。當調用一個 async 函數時,會返回一個 Promise 對象。當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值。
因此你如今知道咯,使用 async 定義的函數,當它被調用時,它返回的實際上是一個Promise對象。
咱們再來看看 await 表達式執行會返回什麼值。
語法:[return_value] = await expression;表達式(express):一個 Promise 對象或者任何要等待的值。
返回值(return_value):返回 Promise 對象的處理結果。若是等待的不是 Promise 對象,則返回該值自己。
因此,當await操做符後面的表達式是一個Promise的時候,它的返回值,實際上就是Promise的回調函數resolve的參數。
明白了這兩個事情後,我還要再囉嗦兩句。咱們都知道Promise是一個當即執行函數,可是他的成功(或失敗:reject)的回調函數resolve倒是一個異步執行的回調。當執行到resolve()時,這個任務會被放入到回調隊列中,等待調用棧有空閒時事件循環再來取走它。
好了鋪墊完這些概念,咱們回過頭看上面那道題目困惑的那兩句關鍵的地方(建議一邊對着題目一邊看解析我怕我講的太快你跟不上啊哈哈😂)。
執行到 async1 這個函數時,首先會打印出「async1 start」(這個不用多說了吧,async 表達式定義的函數也是當即執行的);
而後執行到 await async2(),發現 async2 也是個 async 定義的函數,因此直接執行了「console.log('async2')」,同時async2返回了一個Promise,劃重點:此時返回的Promise會被放入到回調隊列中等待,await會讓出線程(js是單線程還用我介紹嗎),接下來就會跳出 async1函數 繼續往下執行。
而後執行到 new Promise,前面說過了promise是當即執行的,因此先打印出來「promise1」,而後執行到 resolve 的時候,resolve這個任務就被放到回調隊列中(前面都講過了上課要好好聽啊喂)等待,而後跳出Promise繼續往下執行,輸出「script end」。
接下來是重頭戲。同步的事件都循環執行完了,調用棧如今已經空出來了,那麼事件循環就會去回調隊列裏面取任務繼續放到調用棧裏面了。
這時候取到的第一個任務,就是前面 async1 放進去的Promise,執行Promise時發現又遇到了他的真命天子resolve函數,劃重點:這個resolve又會被放入任務隊列繼續等待,而後再次跳出 async1函數 繼續下一個任務。
接下來取到的下一個任務,就是前面 new Promise 放進去的 resolve回調 啦 yohoo~這個resolve被放到調用棧執行,並輸出「promise2」,而後繼續取下一個任務。
後面的事情相信你已經猜到了,沒錯調用棧再次空出來了,事件循環就取到了下一個任務:歷經千辛萬苦終於輪到的那個Promise的resolve回調!!!執行它(啥也不會打印的,由於 async2 並無return東西,因此這個resolve的參數是undefined),此時 await 定義的這個 Promise 已經執行完而且返回告終果,因此能夠繼續往下執行 async1函數 後面的任務了,那就是「console.log('async1 end')」。
謎之困惑的那兩句執行結果(「promise2」、「async1 end」)就是這樣來的~
總結下來這道題目考的,實際上是如下幾個點:
理解了這些,天然就明白了爲何答案是這樣(答出筆試題還要分析給面試官緣由哈哈哈)~
關於調用棧、事件循環、任務隊列能夠點這裏瞭解更詳細的描述。
爲了方便你們直接貼圖:
關於async和await的執行順序這裏也有很詳細的分析能夠參考~
資料參考:
http://www.javashuo.com/article/p-pqnsofgj-ek.html
https://github.com/xitu/gold-miner/blob/master/TODO/how-javascript-works-event-loop-and-the-rise-of-async-programming-5-ways-to-better-coding-with.md