promise、async和await之執行順序的那點事
故事要從一道今日頭條的筆試題說起~ 題目來源:半年工作經驗今日頭條和美團面試題面經分享!!!!!(https://juejin.im/post/5b03e79951882542891913e8)
求列印結果是什麼?
相信是個前端都知道啦,這道題目考的就是 js 裡面的事件循環和回調隊列咯~ 今天題主假設看客都已經了解了 setTimeout 是宏任務會在最後執行的前提(因為它不是今天要討論的重點),我們主要來講講promise、async和await之間的關係。
先上正確答案:
事實上,沒有在控制台執行列印之前,我覺得它應該是這樣輸出的:
為什麼這樣認為呢?因為我們(粗淺地)知道 await 之後的語句會等 await 表達式中的函數執行完得到結果後,才會繼續執行。
MDN是這樣描述 await 的:
async 函數中可能會有 await 表達式,這會使 async 函數暫停執行,等待表達式中的 Promise 解析完成後繼續執行 async 函數並返回解決結果。
會認為輸出結果是以上的樣子,是因為沒有真正理解這句話的含義。
阮一峰老師的解釋我覺得更容易理解:
async 函數返回一個 Promise 對象,當函數執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再接著執行函數體內後面的語句。
對啦就是這樣,MDN 描述的暫停執行,實際上是讓出了線程(跳出 async 函數體)然後繼續執行後面的腳本的。這樣一來我們就明白了,所以我們再看看上面那道題,按照這樣描述那麼他的輸出結果就應該是:
好像哪裡不太對?對比控制台輸出的正確結果,咦~有兩句輸出是不一樣的呀!!
為什麼會這樣呢?這也是這道題目最難理解的一個地方。要搞明白這個事情,我們需要先來回顧一些概念:
async
async function 聲明將定義一個返回 AsyncFunction 對象的非同步函數。
當調用一個 async 函數時,會返回一個 Promise 對象。當這個 async 函數返回一個值時,Promise 的 resolve 方法會負責傳遞這個值;當 async 函數拋出異常時,Promise 的 reject 方法也會傳遞這個異常值。
所以你現在知道咯,使用async定義的函數,當它被調用時,它返回的其實是一個 Promise 對象。 我們再來看看await表達式執行會返回什麼值。
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」)就是這樣來的~
總結
總結下來這道題目考的,其實是以下幾個點:
調用棧
事件循環
任務隊列
promise 的回調函數執行
async 表達式的返回值
await 表達式的作用和返回值
理解了這些,自然就明白了為什麼答案是這樣(答出筆試題還要分析給面試官原因哈哈哈)~
關於調用棧、事件循環、任務隊列可以點這裡(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)了解更詳細的描述。 為了方便大家直接貼圖
關於 async 和 await 的執行順序這裡(https://segmentfault.com/a/1190000011296839)也有很詳細的分析可以參考~
資料參考:
https://segmentfault.com/a/1190000011296839
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
原文:https://lvdingjin.github.io/tech/2018/05/27/async-and-await.html 作者:軍長
※谷歌無奈,Chrome 暫時恢復網頁自動播放功能
※從 0 開始了解 Docker
TAG:JavaScript |