答網友問: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