關於async/await 和 promise的一些問題
前幾天在fcc群裡旁觀水友對一個非同步請求的問題的爭論,爭論的內容不是重點,但他說的一句話引起了我的求知慾
用 async/await 非同步轉同步
之前我的認知是,這是一個generator的語法糖,是用來解決非同步問題的,看起來寫起來是同步程式碼,但實際執行還是非同步的,感覺和他說的有點偏差,有偏差就說明至少有個人錯了;於是我打算重新認識一遍這個東西,說不定會有新收穫。
TALK IS CHEAP, SHOW YOU THE CODE.
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
- async1 end
- promise1
- script end
- promise2
- setTimeout
因為我想await async2這裡就是會等待他返回然後再往下執行
然而其實答案是:
- script start
- async1 start
- async2
- promise1
- script end
- promise2
- async1 end
- setTimeout
這裡就涉及到await的特點了:
await 表示式會暫停當前 async function 的執行,等待 Promise 處理完成。若 Promise 正常處理(fulfilled),其回撥的resolve函式引數作為 await 表示式的值,繼續執行 async function。
若 Promise 處理異常(rejected),await 表示式會把 Promise 的異常原因丟擲。
另外,如果 await 操作符後的表示式的值不是一個 Promise,則返回該值本身。
這裡的暫停執行,我們應該理解為交出執行緒控制權,去執行後面的函式。我們反向來看,假如async函式裡面一直等待await後面的表示式返回,那整個執行緒就等於卡在這個async函式裡了,那不就變成同步阻塞的了。
所以我們回到上面的那段程式碼,開始分析
首先程式碼同步執行輸出 script start
,接著 setTimeout
中的回撥屬於 MacroTask
佇列,會被推入該佇列等待執行。
接著程式碼執行到 async1
函式裡面,輸出 async1 start
,接著遇到了await
關鍵字,這一行語句會從右到左執行,先執行 async2
函式,輸出async2
,因為這個函式沒有返回值,所以返回了一個 Promise.resolve(undefined)
.然後這個 Promise
會被推入 MicroTask
佇列,同時 await
讓出執行緒,跳出 async1
函式,繼續執行下面的同步程式碼。
Promise
構造器中的函式引數會被立即執行,所以輸出 promise1
,然後執行了resolve()
,這個 Promise
也被推入 MicroTask
佇列等待執行,接著輸出script end
,好了,同步程式碼執行完了,呼叫棧現在空了。
然後開始處理非同步的任務佇列。第一個是 async2
中返回的Promise.resolve(undefined)
被resolve之後再次加入佇列,所以回撥沒被執行,第二個是 new Promise
裡resolve的 Promise
,其回撥被執行,輸出 promise2
。好的現在呼叫棧又空了,然後開始再一次處理非同步佇列,這次取到的是之前被第二次放進來的Promise
的回撥,執行之後, 表示式await async2()
的求值就完成了,值是undefined。接著可以認為async2
返回的Promise
已經處理完了,接著執行下面的程式碼,輸出async1 end
。
最後就是MacroTask
佇列中的回撥被執行,輸出setTimeout
這裡需要解釋一下一個地方,async2 返回的Promise狀態是Resolved,為什麼會被放到任務佇列裡面兩次。首先async2函式是一個async function
,他的返回值是一個resolved狀態的Promise,await async2()
類似於await Promise.resolve(something)
,Chrome的V8引擎對這個過程進行了優化,把Promise.resolve()的返回值優化成了返回一個新的Promise,這樣他的回撥就不會被立即執行,而是需要再等一個迴圈才執行。而在Node裡面,Promise.resolve返回的就是當前的Promise,回撥會被立即執行,所以如果你在node裡面執行的話,順序會是這樣的:
- script start
- async1 start
- async2
- promise1
- script end
- async1 end
- promise2
- setTimeout
是因為Node環境下,async2返回的Promise在第一次被推入佇列時就是resolved的,跟new Promise那個是一樣的,所以按照入佇列的順序,先輸出async1 end
。這一點差異,應該以最新V8引擎為準。
詳細解釋可以參考:
async/await 在chrome 環境和 node 環境的 執行結果不一致,求解?
好了,就這樣。