iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

0
Modern Web

讓 TypeScript 成為你全端開發的 ACE!系列 第 50

Day 50. 通用武裝・非同步函式X非同步程序的同步化-TypeScript Generics with Asynchronous Programming III. Async Functions

https://ithelp.ithome.com.tw/upload/images/20191021/20120614wMUuI1f1Nx.png

閱讀本篇文章前,仔細想想看

  1. Generators 使用上有哪些特點?
  2. 積極求值(Eager Evaluation)與惰性求值(Lazy Evaluation)分別特點在哪裡?各自優勢在哪?

另外,本篇承載前一篇文章的內容,強烈建議如果跳到這一篇的讀者務必把前一篇看完啊!

這系列不知不覺剛好到第 50 篇了,感覺接下來是下一篇章的開頭。

不過呢,由於本系列即將要跟出版社接下出書方面的事宜,筆者有考慮把接下來的篇章整合到未來出版的書本裡喔~(畢竟把全部的東西擺在網路上出書就沒啥意義了~);內容也會跟網路文章系列部分雷同但是也有取捨,並且新增自三個月自己實驗 TypeScript 寫小型專案以來的常見經驗與心得。

另外,由於正式出書部分可以寫的內容變多,筆者自認為可以再把本系列的某些原本規劃好卻打掉的細節也寫進來,實在是很迫不及待地開工啊~

如果後續有什麼樣的更新會在本系列補充~~~,所以可以繼續關注一下目前的動態喔~

至於書名是啥,筆者其實回去有 Contemplate 一番~(高級的晶晶體無誤!)

  • 跟本系列名稱一樣:《讓 TypeScript 成為你全端開發的 ACE!》
  • 《射出全端的穿雲箭 —— 我現在要用 TypeScript 出征!》
  • 《學習 TypeScript —— 用 18% 的力量學到 81% 的功力》
  • 《用 TypeScript 反滲透全端的開發生態》

(喂!不要亂搞啊!)

好的~廢話不多說,以下正文開始~

TypeScript 非同步編程第三彈 Asynchronous Programming in TypeScript

回歸正題 —— 非同步的編程到底跟 ES6 Generators 有什麼關係?

有些想知道 Async-Await 的讀者,看到這裡可能以為:“作者是不是忘記要講 Async-Await 的由來了?我到底還要看這東西看多久?”

本篇就會討論到了啦!但是前面的東西不講,讀者可能也沒法體會 Async-Await 從 Promise Chain 到 Generators 再變成 Async-Await 的演變過程 —— 對,Generators 這個的東西真的很重要!

筆者再把演變的口訣提出來稍微做些前情提要:

啟始於 Callback Hell,攤平為 Promise Chain,乘載於 Generator Functions,演變成 Async Functions

Originated from Callback Hell, flattened into Promise Chain, combined with Generator Functions, evoluted as Aysnchronous Functions.

好的,我們接下來要 Focus 在這句話上:

乘載於 Generator Functions

貼心小提示

以下的難度會超級、巨幅地提升(筆者不開玩笑),讀者不需要看得懂程式碼過程 —— 只要記得演變過程與結論,心理有數就夠了

首先,既然是從 Promise Chain 開始,承載於 Generators 的特點,這到底意味是什麼呢?

走一下筆者的思路 —— 筆者希望把 Promise Chain:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614r19NhEZRe7.png

將裡面的 request 看作成迭代器的元素 —— 讀者看到這裡可能會疑惑,但事實上筆者想要將 Promise Chain 改寫成:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614iTIotx2DYs.png

首先,如果第一次建立此 sendRequestGenerator 產生的迭代器,每一次 next 就相當於 yield 出一個 Promise<T> 物件,並且下一次呼叫 next 時填入某個值 —— 該值就會取代 yield request 部分並且指派到 response 變數去。

更簡單的想法是:

每一次 yield request 出去時(Pull 出一個 Promise<T> 物件)—— 會將該 requestresponse 回傳回來(Push 進該 Promise<T> resolve 出來的結果)

以下是更完整一點的範例程式碼,以下用 Promise Chain 表示形式示範。(結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20191018/20120614pIGVopzGb6.png

https://ithelp.ithome.com.tw/upload/images/20191018/20120614LqIp6Bn7JW.png
圖ㄧ:Promise Chain 連續執行 pingRequest 的結果

另外,將 Promise Chain 轉換成 Generator 表示形式如下:

https://ithelp.ithome.com.tw/upload/images/20191018/201206149H8LAKowlU.png

這樣的形式是不是優雅許多~ 不過呢,相信敏銳的讀者早已察覺到,直接去 Run 這個 Generator 肯定沒用,勢必要用手動的方式去建立迭代器後,再不停地用 next 方法呼叫、迭代 Promise<T> 物件。以下這段程式碼,非常麻煩,但是測試結果(如圖二)ㄧ樣可以達到跟 Promise Chain 的效果。

https://ithelp.ithome.com.tw/upload/images/20191018/20120614StvE1pLruZ.png

https://ithelp.ithome.com.tw/upload/images/20191018/201206146RbA2R1S9x.png
圖二:使用 Generator 也可以達到跟 Promise Chain 相同的效果

這裡筆者就不強迫讀者要看得懂以上的過程(除非你有興趣與熱忱),但筆者想要跟讀者講的重點是:

使用 Generator 可以達到跟 Promise Chain 相同的效果

可是讀者一定會問:“作者你跟我開玩笑?裡面用超多層回呼函式的,根本比 Promise Chain 更悲劇啊!”

事實上呢,以上的那一段過程可以用遞迴(Recursion)的方式表示(以下程式碼的運行結果如圖三)。

https://ithelp.ithome.com.tw/upload/images/20191018/201206142ED2sby9tE.png

https://ithelp.ithome.com.tw/upload/images/20191018/20120614VDKRjtB1Ii.png
圖三:使用遞迴可以將 Generator 中的不同 Request Run 起來~

所以呢,筆者證明了:

可以藉由 Generators 的 Push-Pull 模式的概念 —— 結合遞迴(Recursion)的技巧,完整地實現非同步程序的運行

另外,這樣的 Generators 寫法比起 Promise Chain 更有好處的原因在於,你可以用平常寫指令式程式碼的想法,簡簡單單地進行 Error Handling 的邏輯:

https://ithelp.ithome.com.tw/upload/images/20191018/20120614LwV3UIatHV.png

以上使用 try...catch... 的方式,在 Promise Chain 幾乎很難做到,但是使用 Generator 就可以盡情地使用 try...catch... 語法 —— 處理錯誤方面的邏輯會更加簡單呢!

基本上,以程式設計的角度 —— 讀者可以這麼想:

Promise Chain 把非同步運作的邏輯過程用很強硬的方式直接串聯在一起,錯誤處理又必須要被隔離到.catch 部分,造成功能必須要強行被拆解掉。

如果想要在 Promise Chain 中途進行任何處理動作,就必須得再中間多串一些 then 等等的操作。

而 Generators 則是把 Promise Chain 中的每個 Promise 拆成迭代器中的一個個元素,元素與元素中間可以自由添加任何邏輯,包含錯誤處理(Error Handling)等。

這使得我們可以不需要使用 Promise API,而是很直觀的編寫程式的方式去寫出 response = yield promiseObject 之類的邏輯結合 try...catch... —— 可以專心在程序的敘述過程

另外,Generators 比起 Promise Chain,多了一個 runGenerator 函式,該函式只是負責將 Generator 的運作過成抽離出來而已。

重點 1. Generators 在非同步編程的演變過程中的角色

運用 Push-Pull Model 的性質 —— 結合遞迴(Recursion)的手法,我們可以取代 Promise Chain 的語法行為。

而 Generators 的寫法好處在於,我們可以自由地在 Generators 內部:

  • response 進行 Data Reshaping 的邏輯
  • 進行 try...catch... 相關的錯誤處理
  • 甚至你也可以 yield Promise.all([...]) 對多個請求進行平行化處理

讀者也可以參考 tj/co 這個 GitHub Repo.,筆者就是採用類似這個 Repo. 的手法展示 —— 如何使用 Generator 達成非同步的程式碼序列執行的過程。

漫長的 Generators 演變歷程就到此結尾,最後就是 Async Function 的演變結果還沒被筆者講完喔~!

最後的一塊拼圖前的 Recap.

在非同步的程式運作過程中,通常最簡單的處理方式是使用回呼函式 —— 當非同步程序執行完畢時,再呼叫回呼函式處理。

但多層的回呼函式會造成程式碼:

  • 層層疊疊非常混亂
  • 錯誤處理會非常麻煩
  • 不同區塊的非同步程式耦合度超高,不易進行功能上的拆解重複再利用

這時,ES6 Promise 將這個非同步程序使用類似 State Machine 的方式 —— 使得處理非同步程序從巢狀回呼函式,攤平成一系列的 Promise Chain,解決了:

  • 巢狀語法較不易快速理解
  • 不同區塊的非同步程式碼可以解耦(Decouple)成一個個 Promise 物件,自由串在一起

但 Promise 物件仍然在語法的寫法上還是稍嫌麻煩:

  • Promise Chain 比較不 DRY 的地方在於 —— then 這個東西出現次數超多
  • 錯誤處理還是稍嫌麻煩些,儘管可以隔離掉,但是沒有像 try...catch... 語法方式,直觀地寫得特別清楚

ES6 Generators 為迭代器的產生函式,透過 Push-Pull Model 方法以及惰性求值(Lazy Evaluation)的概念 —— 我們可以將不同區塊的非同步程式碼,使用 Promise 包裝起來外,將每個 Promise 視為迭代器裡的元素,讓它可以在 Generator 進行迭代,並且在外部使用遞迴(Recursion)的方式,同樣可以模擬出非同步程式依序執行的情境。

而 Generator 的好處就是:

  • Promise Chain 被串起來的行為再被抽象化成外部的遞迴演算法,因此我們可以專心處理非同步程式碼內部的主程序
  • 裡面可以將非同步程式碼,藉由 yield 方式,寫得很像同步(Synchronous)的程式碼
  • 錯誤處理更直覺,可以直接使用 try...catch... 敘述式

ES7 Async-Await 的運作機制

啟始於 Callback Hell,攤平為 Promise Chain,乘載於 Generator Functions,演變成 Async Functions

Originated from Callback Hell, flattened into Promise Chain, combined with Generator Functions, evoluted as Aysnchronous Functions.

事實上,這個演變非常簡單啊!(而且也可能令人感到白癡!)

就是把 Promise Chain 換作的 Generator 形式:

  • 將 Generator Function 改成 async function 寫法
  • yield 關鍵字取代成 await

所以以下這個程式碼中的 async function 形式等效於 Generator 的手法。

https://ithelp.ithome.com.tw/upload/images/20191021/20120614mxwEhcjKQb.png

但請切記,Generator 必須要有筆者宣告過的遞迴迭代方式去處理 Request —— 然而,Async Await 語法就已經將該邏輯幫你做掉了

另外,由於 Async Await 是非同步的語法,自然而然輸出的東西會是 —— 你應該想得到的 —— Promise 物件! (推論結果如圖四)

https://ithelp.ithome.com.tw/upload/images/20191021/201206148nGv1qhEwc.png
圖四:輸出為 Promise<void> 則是因為,該 Async Function return 的東西為空,因此才是 void 呢!

所以讀者也不需要擔心說 —— 如果想要寫成 Generator,還要自己客製化遞迴函式來處理該 Generator 的流程。

不用那麼麻煩!

Async functions comes to the rescue!

使用 Async Await Function 的手法很簡單,就很像是在呼叫 Promise 物件ㄧ樣。(以下程式碼執行結果如圖五)

https://ithelp.ithome.com.tw/upload/images/20191021/20120614zANpPBHV3y.png

https://ithelp.ithome.com.tw/upload/images/20191021/20120614EloShy09gE.png
圖五:就是當成 Promise 物件ㄧ樣在執行,差別在於它是函式,必須要呼叫

不過你倒是也可以將其想成它是回傳 Promise 物件的函式也可以 —— 如果遇到以下的程式碼,猜猜看會如何執行?

https://ithelp.ithome.com.tw/upload/images/20191021/20120614AV0W35OENn.png

畢竟本系列是 TypeScript 系列,我們當然要強調 TypeScript 的好處 —— 型別的推論(Type Inference)是很棒的工具!

請看推論結果。(圖六)

https://ithelp.ithome.com.tw/upload/images/20191021/20120614us3H8VshyB.png
圖六:看到了沒~Async Function 回傳的值,儘管不是非同步程序相關的東西,如 Promise 物件,但回傳值都會包裝一層 Promise。

另外,你也可以在 Async Function 執行另一個 Async Function,畢竟 await 都是等待 Promise 相關的非同步程序的結果,而 Async Function 回傳的結果都會是 Promise 物件。

由於細節討論下去太多,筆者只 Cover 重點演變部分,剩下的語法過程驗證一方面是直接請讀者讀 Doc,一方面就是試試看筆者列出來的一些案例。

以下筆者就在讀者試試看的部分列出一些案例,讓讀者可以去試試看這些程式碼會怎麼運作。

讀者試試看

以下範例裡使用到的 delayAfter<T>(milliseconds: number, value: T): Promise<T> 函式定義如下:
https://ithelp.ithome.com.tw/upload/images/20191021/20120614gDEntz4fEc.png

  1. 請問以下程式碼大概會如何運作?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614t5Y6yzdJ9q.png

  2. 請問以下程式碼大概會如何運作? TypeScript 會在 await 關鍵字部分出現什麼樣的訊息提醒你?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614bfW5uRu2WB.png

  3. 請問以下的程式碼,會印出什麼樣的結果,並且執行時間為何?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614WTQQ5pwzVn.png

  4. 請問以下的程式碼,會印出什麼樣的結果,並且執行時間為何?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614pd78l20cyv.png

  5. 請問以下的程式碼,差異在哪?
    https://ithelp.ithome.com.tw/upload/images/20191021/20120614nuwkeyea7g.png

  6. 請問以下的程式碼會如何運作?
    https://ithelp.ithome.com.tw/upload/images/20191021/201206147IcqEDiHnE.png

  7. 如果想要設計一個 Request Timeout Feature,並且進行錯誤處理,你會如何運用 Async Function 與 Promise 設計?

  8. 如果 async function 被積極註記輸出為 Promise<number> 代表什麼意思?若 async function 被積極註記為非 Promise<T> 類型的物件會發生什麼事?

重點 2. 非同步函式 Asynchronous Function

自 Generator 結合 Promise Chain 的概念演化出來的結果,為 ES7 的標準。

非同步函式可以用同步的語法撰寫各種非同步行為,使得:

  • 程式碼可讀性大幅提升
  • 提供另一種方式操作 Promise 物件,可以使用 await 來等待 Resolve 出來的結果
  • 錯誤處理更直觀,可以直接使用 try...catch... 敘述式寫非同步程序的錯誤處理部分
  • 非同步程序可以進行高度抽象化
  • 非同步程序又可以互相用 await 組合(await 其他 Async Function 的執行結果)
  • await Promise.all 可以等待多個非同步程序的執行結果
  • await Promise.race 可以等待多個非同步程序的其中一項最先執行完畢的結果

非同步函式的推論結果必須要是 Promise<T> 類型的結果,如果積極註記為非 Promise<T> 相關型別的物件,TypeScript 會出現警告!

非同步函式跟普通的 Promise 物件差別在於 —— 非同步函式是函式,Promise 是物件 —— 非同步函式必須要有括弧進行呼叫的動作。(這應該是廢話

小結

筆者已經把非同步編程部分大致上已經 Cover 完畢囉~

沒意外這應該會是本系列的結尾,也是本系列篇章《通用武裝》的結尾~

接下來是不是該出《進化實驗》篇章呢?也就是 Decorator 相關的語法與用途~ 這裡就留待未來出書內容進行詳解吧~ 因為後面要寫的東西可不是開玩笑的簡單,書本可以做得到的事情讓書本來補充

也感謝支持本系列的讀者們以及舉辦第 11 屆鐵人賽的主辦單位 —— IT邦幫忙團隊,認可本系列為 Modern Web 組的其中一個冠軍文章系列~ (好像有點太晚 Celebrate?

但筆者還是補充一下,未來出的書本中,裡面不會出現的內容,本系列文章會再繼續更新下去,所以可以照樣關注下去喔~


上一篇
Day 49. 通用武裝・非同步迭代 X 無窮地惰性求值 - TypeScript Generics with Asynchronous Programming II. ES6 Generators
下一篇
Day 50+ 用了會上癮的 TypeScript 新功能 - Easily Addicted New Features in TypeScript
系列文
讓 TypeScript 成為你全端開發的 ACE!51

尚未有邦友留言

立即登入留言