8 張圖幫你一步步看清 async,await 和 promise 的執行順序

為什麼寫這篇文章?

說實話,關於js的異步執行順序,宏任務、微任務這些,或者async/await這些慨念已經有非常多的文章寫了。


但是怎麼說呢,簡單來說,業務中很少用async,不太懂async呢,

研究了一天,感覺懂了,所手癢想寫一篇 ,哈哈

畢竟自己學會的知識,如果連表達清楚都做不到,怎麼能指望自己用好它呢?

測試一下自己有沒有必要看

所以我寫這個的文章,主要還是交流學習,如果您已經清楚了eventloop/async/await/promise 這些東西呢,可以 break 啦。

那麼還是先通過一道題自我檢測一下,是否有必要繼續看下去把。

其實呢,這是去年一道爛大街的「今日頭條」的面試題 。

我覺得這道題的關鍵,不僅是說出正確的打印順序,更重要的能否說清楚每一個步驟,為什麼這樣執行。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

注:因為是一道前端面試題,所以答案是以瀏覽器的eventloop機制為準的,在node平臺上運行會有差異。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

如果你發現運行結果跟自己想的一樣,可以選擇跳過這篇文章啦,

或者如果你有興趣看看俺倆的理解有沒有區別,可以跳到後面的 「畫圖講解的部分」

需要具備的前置知識

  • promise的使用經驗

  • 瀏覽器端的eventloop

不過如果是對 ES7 的 async 不太熟悉,是沒關係的哈,因為這篇文章會詳解 async。

那麼如果不具備這些知識呢,推薦幾篇我覺得講得比較清楚的文章

  • https://segmentfault.com/a/1190000012806637:這是我之前寫的講解eventloop的文章,我覺得還算清晰,但是沒涉及 async

  • https://segmentfault.com/a/1190000007535316:這是我讀過的講async await最清楚的文章

  • http://es6.ruanyifeng.com/#docs/promise:promise就推薦阮一峰老師的ES6吧,不過不熟悉 promise 的應該較少啦。

主要內容

第1部分:對於async await的理解

我推薦的那篇文章,對 async/await 講得更詳細。不過我希望自己能更加精煉的幫你理解它們。

這部分,主要會講解 3 點內容:

1、async 做一件什麼事情?

2、await 在等什麼?

3、await 等到之後,做了一件什麼事情?

4、補充: async/await 比 promise有哪些優勢?(回頭補充)

1、async 做一件什麼事情?

一句話概括: 帶 async 關鍵字的函數,它使得你的函數的返回值必定是 promise 對象。

也就是,如果async關鍵字函數返回的不是promise,會自動用 Promise.resolve() 包裝。

如果async關鍵字函數顯式地返回promise,那就以你返回的promise為準。

這是一個簡單的例子,可以看到 async 關鍵字函數和普通函數的返回值的區別:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

所以你看,async 函數也沒啥了不起的,以後看到帶有 async 關鍵字的函數也不用慌張,你就想它無非就是把return值包裝了一下,其他就跟普通函數一樣。

關於async關鍵字還有那些要注意的?

  • 在語義上要理解,async表示函數內部有異步操作

  • 另外注意,一般 await 關鍵字要在 async 關鍵字函數的內部,await 寫在外面會報錯。

2、await 在等什麼?

一句話概括: await等的是右側「表達式」的結果。

也就是說,右側如果是函數,那麼函數的return值就是「表達式的結果」。

右側如果是一個 'hello' 或者什麼值,那表達式的結果就是 'hello'。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

這裡注意一點,可能大家都知道await會讓出線程,阻塞後面的代碼,那麼上面例子中, async2 和>

是從左向右執行,一旦碰到await直接跳出,阻塞 async2() 的執行?

還是從右向左,先執行async2後,發現有await關鍵字,於是讓出線程,阻塞代碼呢?

實踐的結論是,從右向左的。先打印async2,後打印的>

之所以提一嘴,是因為我經常看到這樣的說法,「一旦遇到await就立刻讓出線程,阻塞後面的代碼」。

這樣的說法,會讓我誤以為,await後面那個函數, async2()也直接被阻塞呢。

3、await 等到之後,做了一件什麼事情?

那麼右側表達式的結果,就是await要等的東西。

等到之後,對於await來說,分2個情況:

  • 不是promise對象

  • 是promise對象

如果不是 promise , await會阻塞後面的代碼,先執行async外面的同步代碼,同步代碼執行完,再回到async內部,把這個非promise的東西,作為 await表達式的結果。

如果它等到的是一個 promise 對象,await 也會暫停async後面的代碼,先執行async外面的同步代碼,等著 Promise 對象 fulfilled,然後把 resolve 的參數作為 await 表達式的運算結果。

第2部分:畫圖一步步看清宏任務、微任務的執行過程

我們以開篇的經典面試題為例,分析這個例子中的宏任務和微任務。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

先分享一個我個人理解的宏任務和微任務的慨念,在我腦海中宏任務和為微任務如圖所示:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

也就是「宏任務」、「微任務」都是隊列。

一段代碼執行時,會先執行宏任務中的同步代碼:

  • 如果執行中遇到setTimeout之類宏任務,那麼就把這個setTimeout內部的函數推入「宏任務的隊列」中,下一輪宏任務執行時調用。

  • 如果執行中遇到promise.then()之類的微任務,就會推入到「當前宏任務的微任務隊列」中,在本輪宏任務的同步代碼執行都完成後,依次執行所有的微任務1、2、3。

下面就以面試題為例子,分析這段代碼的執行順序。

每次宏任務和微任務發生變化,我都會畫一個圖來表示他們的變化。

直接打印同步代碼 console.log('script start')

首先是2個函數聲明,雖然有async關鍵字,但不是調用我們就不看。然後首先是打印同步代碼 console.log('script start')。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

將setTimeout放入宏任務隊列

默認 <script> 所包裹的代碼,其實可以理解為是第一個宏任務,所以這裡是宏任務2:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

調用async1,打印 同步代碼 console.log('async1 start')

我們說過看到帶有async關鍵字的函數,不用害怕,它的僅僅是把return值包裝成了promise,其他並沒有什麼不同的地方。所以就很普通的打印 console.log('async1 start')。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

分析一下 awaitasync2()

前文提過await,它先計算出右側的結果,然後看到await後,中斷async函數:

  • 先得到await右側表達式的結果。執行async2(),打印同步代碼console.log('async2'),並且returnPromise.resolve(undefined)。

  • await後,中斷async函數,先執行async外的同步代碼。

目前就直接打印 console.log('async2'):

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

被阻塞後,要執行async之外的代碼。

執行 newPromise()

Promise構造函數是直接調用的同步代碼,所以 console.log('promise1'):

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

代碼運行到 promise.then()

代碼運行到promise.then(),發現這個是微任務,所以暫時不打印,只是推入當前宏任務的微任務隊列中。

注意:這裡只是把promise2推入微任務隊列,並沒有執行。微任務會在當前宏任務的同步代碼執行完畢,才會依次執行:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

打印同步代碼 console.log('script end')

沒什麼好說的。執行完這個同步代碼後,「async外的代碼」終於走了一遍

下面該回到 await 表達式那裡,執行 awaitPromise.resolve(undefined) 了。

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

回到async內部,執行 awaitPromise.resolve(undefined)

這部分可能不太好理解,我儘量表達我的想法。

對於 awaitPromise.resolve(undefined) 如何理解呢?

https://developer.mozilla.org/zh-

CN/docs/Web/JavaScript/Reference/Operators/await

根據 MDN 原話我們知道:如果一個 Promise 被傳遞給一個 await 操作符,await 將等待 Promise 正常處理完成並返回其處理結果。

在我們這個例子中,就是 Promise.resolve(undefined) 正常處理完成,並返回其處理結果。那麼 awaitasync2() 就算是執行結束了。

目前這個promise的狀態是fulfilled,等其處理結果返回就可以執行await下面的代碼了。

那何時能拿到處理結果呢?

回憶平時我們用promise,調用resolve後,何時能拿到處理結果?是不是需要在then的第一個參數裡,才能拿到結果。

(調用resolve時,會把then的參數推入微任務隊列,等主線程空閒時,再調用它)。

所以這裡的 awaitPromise.resolve() 就類似於:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

把then的第一個回調參數 (undefined)=>{} 推入微任務隊列。

then執行完,才是 awaitasync2() 執行結束。

awaitasync2() 執行結束,才能繼續執行後面的代碼,如圖:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

此時當前宏任務1都執行完了,要處理微任務隊列裡的代碼。

微任務隊列,先進選出的原則:

  • 執行微任務1,打印promise2

  • 執行微任務2,沒什麼內容..

但是微任務2執行後, awaitasync2() 語句結束,後面的代碼不再被阻塞,所以打印:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

宏任務1執行完成後,執行宏任務2

宏任務2的執行比較簡單,就是打印:

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

補充在不同瀏覽器上的測試結果

谷歌瀏覽器,目前是版本是「版本 71.0.3578.80(正式版本) (64 位)」 Mac操作系統

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

Safari瀏覽器的測試結果

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

火狐瀏覽器的測試結果

8 張圖幫你一步步看清 async,await 和 promise 的執行順序

如果不理解可以留言,有錯誤的話也歡迎指正。


來自:https://segmentfault.com/a/1190000017224799


分享到:


相關文章: