promise、async和await之執行順序的那點事

軍長發表於2019-03-03

原文地址

故事要從一道今日頭條的筆試題說起~
題目來源:半年工作經驗今日頭條和美團面試題面經分享!!!!!

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')
複製程式碼

求列印結果是什麼?

相信是個前端都知道啦,這道題目考的就是js裡面的事件迴圈和回撥佇列咯~
今天題主假設看客都已經瞭解了setTimeout是巨集任務會在最後執行的前提(因為它不是今天要討論的重點),我們主要來講講promiseasyncawait之間的關係。

先上正確答案:

script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
複製程式碼

事實上,沒有在控制檯執行列印之前,我覺得它應該是這樣輸出的:

script start
async1 start
async2
async1 end
promise1
script end
promise2
setTimeout
複製程式碼

為什麼這樣認為呢?因為我們(粗淺地)知道await之後的語句會等await表示式中的函式執行完得到結果後,才會繼續執行。

MDN是這樣描述await的:

async 函式中可能會有 await 表示式,這會使 async 函式暫停執行,等待表示式中的 Promise 解析完成後繼續執行 async 函式並返回解決結果。

會認為輸出結果是以上的樣子,是因為沒有真正理解這句話的含義。

阮一峰老師的解釋我覺得更容易理解:

async 函式返回一個 Promise 物件,當函式執行的時候,一旦遇到 await 就會先返回,等到觸發的非同步操作完成,再接著執行函式體內後面的語句。

對啦就是這樣,MDN描述的暫停執行,實際上是**讓出了執行緒(跳出async函式體)**然後繼續執行後面的指令碼的。這樣一來我們就明白了,所以我們再看看上面那道題,按照這樣描述那麼他的輸出結果就應該是:

script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
複製程式碼

好像哪裡不太對?對比控制檯輸出的正確結果,咦~有兩句輸出是不一樣的呀!!

async1 end
promise2
複製程式碼

為什麼會這樣呢?這也是這道題目最難理解的一個地方。要搞明白這個事情,我們需要先來回顧一些概念:

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”)就是這樣來的~

總結

總結下來這道題目考的,其實是以下幾個點:

  1. 呼叫棧
  2. 事件迴圈
  3. 任務佇列
  4. promise的回撥函式執行
  5. async表示式的返回值
  6. await表示式的作用和返回值

理解了這些,自然就明白了為什麼答案是這樣(答出筆試題還要分析給面試官原因哈哈哈)~

關於呼叫棧、事件迴圈、任務佇列可以點這裡瞭解更詳細的描述。
為了方便大家直接貼圖:

tupian

關於async和await的執行順序這裡也有很詳細的分析可以參考~

資料參考:
segmentfault.com/a/119000001…
github.com/xitu/gold-m…

相關文章