答網友問:Await 一個 Promise 對象到底發生了什麼

語言: CN / TW / HK

大家好,我是二哥。

前兩篇文章發出來後,有一些網友在後台諮詢我一些問題,我把它們歸總羅列在一起。這篇文章既是答網友問也是對前兩篇的補充和複習。

先放下前兩篇的鏈接。

圖解 Node.js 的核心 event-loop

​多圖剖析公式 async=Promise+Generator+自動執行器​

圖 1:async 函數代碼示例

問 0 :上一篇所提到的 generator 和自動執行器是運行在不同的線程裏面嗎?

答 0 :無論是 generator 還是自動執行器,都是在 event-loop 線程也就是運行 JS code 的主線程裏面運行的。再強調一遍:它倆不是在兩個線程裏面運行的。

讓我們再看一遍 Node.js 官網對 event-loop 的描述。它強調了一個重點:JS code 是以單線程方式被執行的。

The event loop is what allows Node.js to perform non-blocking I/O operations — despite the fact that JavaScript is single-threaded — by offloading operations to the system kernel whenever possible.

問 1 :await p 這條語句產生了異步請求了嗎?

答 1 :不,它沒有。await 只是在等待 p 狀態的改變,無論狀態是從 pending 變成 resolved 還是從 pending 變為 rejected 。

問 2 :那異步請求是什麼時候產生的?

答 2 :是在 Promise 的 executor 裏面,執行 setTimeout 時產生的。

​下文把 new Promise() 時傳遞進去的 callback (resolve, reject)=>{ /* your code */} 稱為 executor 。其中參數 resolve 和 reject 是由 Promise 自己實現的。需要注意的是這個 executor 是在 new Promise() 的時候,立即執行的。

假如我們在 executor 裏面執行的是 fs.read(fd[, options], callback) 這樣的語句,那類似地,異步請求是在調用 fs.read() 時產生的。​

問 3 :p 狀態改變後,為什麼通過 resolve(200) 傳遞的 200 會變成變量 res 的求值結果?

答 3 :這就是為什麼説我們需要了解 await 背後的實現原理。我們藉助圖 2 和圖 4 來複習一下。

如圖 2 所示,async 函數首先轉換成了 generator 函數。但 generator 函數自己是不能自動運行的,所以得搭配一個自動執行器,驅動它往前走。自動執行器如同慈愛的媽媽,而 generator 就像那個懵懂的幼兒。小孩子每走一段路都會停下來,回頭看看在他身後寸步不離的媽媽,得到媽媽的鼓勵或者獎勵後,再走向下一個目標。

圖 2:async 函數轉換成 generator 函數示例

在講解圖 4 之前,還是有必要再次複習兩個重要的概念:yield 表達式和 yield 語句。如圖 3 所示:

  • a+b 是表達式,它的求值結果影響到的是 { value: xxx, done: xxx } 中的 value 屬性,而 { value: xxx, done: xxx } 是調用者通過迭代器調用 next() 方法的返回值 。
  • yield a+b 是 yield 語句,調用者可以通過給 next() 方法傳實參來影響 yield 語句的返回值。比如 next(200) 則會使得變量 a1 為 200 。

圖 3 還畫出了一個重要的地方:generator 函數執行的暫停點:在 yield 表達式求值結束之後,但 yield 語句返回之前。

圖 3:yield 表達式和 yield 語句對比

為了更好更清晰地回答問題 3,二哥給大家畫了圖 4 。

​這一步開始通過執行器調用 generator。

② 雖然對 generator 真正的調用發生在這裏,但 generator 函數在 ② 這步其實什麼都沒有做,只是立即返回了一個迭代器。

③ 自動執行器從這裏開始進入驅動 generator 模式。③ 這一步沒有給形參 data​ 賦值,因為我們不能在第一次執行  g.next() 的時候給它注入一個值。

④ 這一步每調用一次  g.next() 就會使得 generator 從上次暫停於 yield 的位置開始運行,直到再次遇到 yield 。

⑤ 所以第一次對  g.next() 調用使得左側 generator 函數從函數起始位置一直運行直到遇到 yield 。

我們看到 ⑤ 所標識出來的代碼執行過程其實是創建了一個 Promise 對象,且在 Promise 的 executor 裏面設置了一個 1s 鐘的定時器。注意,這個 executor 是在創建 Promise 對象時立即執行的,不過 ⑦ 處的代碼要等到 1s 之後才會執行。

⑥ generator 函數暫停之前,先會將 yield 表達式的求值結果通過 { value: xxx, done: xxx} 返回給  g.next() 調用方,也即右圖 ④ 位置。

所以你一定猜到了,右圖 ④ 位置的變量 result 為 { value: p, done: false} 。這裏的 p 就是 ⑤ 執行過程中產生的 Promise 對象。

通過這樣的方式,Promise 對象在 generator 函數和自動執行器之間流轉。真是一個巧妙的過程。

那麼你在右側 ⑧ 處看到 result.value.then(callback) 這樣的語句就不會感到納悶了,這是 Promise 的標準用法。當 p 的狀態變成 resolved 後,⑧ 處的 callback 自然就會得到運行的機會了。

⑦ 1s 很快,滴答一下過去後,resolve(200) 得以運行。它的運行使得 p 的狀態變成 resolved,所以在  ⑧ 處耐心等待的 callback 開始了它的工作。

⑧ 是的,這個時候 data 的值為 200 。這是再自然不過的事,如果你對 Promise 的使用瞭然於胸的話。

⑨ 自動執行器又一次執行 next(data)​ 。不過這一次給它傳了一個實參 200 。所以這一次 ④ 處執行的代碼變為: g.next(200) 。

⑩ 自動執行器執行  g.next(200) 必然會驅使 generator 函數動身繼續往前趕路。

還記得 generator 函數上次停在哪裏休息的嗎?對,左側 ⑤ 處箭頭所指的位置。generator 函數恢復運行後乾的第一件事就是對 yield 語句求值。

如果像   g.next()​  這樣驅動它的話,yield 語句返回的是 undefined 。不過這次我們不一樣,因為我們執行的是  g.next(200) 。很巧妙,傳給 next() 的實參  200 作為 yield 語句的返回值賦值給了左側變量 res​

圖 4:generator + 自動執行器細節圖

讓我們再回頭看下圖 1 的示例代碼,我們來做個總結:

  1. await p 語句是個糖衣,它包裹的是 yield p 語句 + 自動執行器。
  2. 所謂 await p 暫停並不是説主線程執行  JS  code 暫停了。相反主線程還在繼續執行其它的 JS code 。
  3. await 是在等待 p 的狀態發生變化。這個等待時間有多長?這完全取決於創建 p 的時候,  executor 裏面何時會調用 resolve() 或 reject() 。
  4. 執行 await p 語句的時候,無論 p 的狀態是否已經發生了變化,執行到 await p 都會導致 V8 engine 轉而去自動執行器裏面執行。這是 yield p 語句使然。
  5. 動執行器如同一個如影隨形的媽媽,她拿到 p 之後,會耐心地等待,直到得到 p 狀態改變後的 value 。最後再通過 g.next(value) 把 value 返回給它摯愛的 generator 函數。

圖 5:同圖 1