iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

3
Modern Web

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

Day 48. 通用武裝・非同步概念 X 脫離巢狀地獄 - TypeScript Generics with Asynchronous Programming I. Promise Chain

https://ithelp.ithome.com.tw/upload/images/20191020/20120614o1AXuHcDdg.png

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

  1. ES6 MapSet 在 TypeScript 裡使用時需要注意的事項。
  2. ES6 Promise 的基本運作機制為何?
  3. ES6 Promise 在 TypeScript 裡使用時需要注意的事項。

如果還不清楚可以看一下前一篇文章喔~

事實上 ES2015+ 與泛型機制的主題還沒結束,最有看頭的應該是 TypeScript 結合非同步語法的編程,這應該對普遍入門 TypeScript 的讀者來說也是很重要的課題。

對任何 JavaScript 開發者,同步與非同步在 JS 的機制也是重點之一,萬一如果遇到職場面試之類的,或多或少會問到關於這方面的問題。

而 ES6 Generators 的應用層面,事實上對於導出 Async-Await 的語法是一環很重要的元素,這也會被筆者提到。

由於 ES6 Generators 與 ES7 Async-Await 的難度相對稍微高一些,因此筆者也會重新討論這些東西到底在做什麼。

因此,就算你是不熟悉這些 Feature 的讀者們也不需要擔心,因為筆者會從頭講到尾 XD。由於這是個大主題,請讀者耐心地領會。

以下正文開始

TypeScript 非同步編程 Asynchronous Programming in TypeScript

同步 V.S. 非同步的概念 Sync. V.S. Async. in JavaScript

同步與非同步的概念 —— 相信早已在 JavaScript 圈已經是熱門無法再繼續補充的觀念 —— 因為都已經被熱心的社群講完了。(筆者只能講 P 話

但筆者依舊認為,非同步的觀念在學習曲線上,依舊讓大部分學習 JS 的人感到困難,事實上筆者也是花了好一段時間才理解 Event Loop 到底在做什麼,因此這裡筆者會稍微講解一下差異到底在哪,但筆者依舊鼓勵讀者好好參照多方資源學習。

同步的概念

同步的概念很簡單,相信任何初學 JavaScript 的讀者早就遇過好一陣子了,只要是依照順序執行的方式即同步的行為(Sequentially Executed)。

let a = 3;
let b = 5;

let sum = a + b;
let power = Math.pow(a, b);

以上的範例程式碼在 JS 引擎裡一定會是同步的執行狀態,如下:

  • 第一行將數字 3 指派到變數 a,處理好 Memory Allocation 後換下一行
  • 第二行將數字 5 指派到變數 b,處理好 Memory Allocation 後換下一行
  • 第三行為空,換下一行
  • 第四行遇到 + 運算子,將變數 ab 進行 + 運算後,指派結果到變數 sum,處理好 Memory Allocation 後換下一行
  • 第五行遇到 Math.pow 方法的呼叫,將 ab 值代入到該方法後,指派輸出結果到變數 power,處理好 Memory Allocation 後換下一行
  • 沒有下一行,程序結束

非同步的概念

接下來就比較麻煩一點 —— 非同步程序在 JS 的運作機制過程,讀者可能以為:不就只是單純譬如 setTimeout 或者是 Event Listener 之類的東西嗎?這有什麼好講的?

不過講白一點,非同步的概念就是跟同步相反,不按照程式碼執行的順序執行(這是很大的廢話)。

反過來說,問題在於:那非同步的程式碼到底是怎麼樣的不按照程式碼順序執行 XD?

一種是回呼函式(Callback Function)的方式處理,比如說你呼叫了一個方法,該方法可能蘊藏一些複雜運算,然後你會想要等該方法結束掉時觸發某些程序,才會傳入 Callback Function。

事件的監聽也是非同步程式碼的一環,事實上這會牽扯到幾個重要主角:Call Stack、Event Table、Event Queue 以及 Event Loop,以下筆者慢慢對 JS 事件的處理概念進行展開。

1. Call Stack

在 JS 的世界裡,變數作用域只有分兩種 —— 全域(Global Scope)與函式作用域(Functional Scope)。

每一個作用域都有屬於自己的執行背景(Execution Context)。

Execution Context 泛指程式碼執行到的位置之背景資料狀況,比如說,在函式作用域內宣告變數 a,該 a 的資料會紀錄在該函式作用域內的執行背景;一但該函式執行結束回到全域時,該函式執行背景會移除,所以變數 a 的資料也會被清掉,全域的執行背景就沒有了變數 a 的資料 —— 換句話說,全域的執行背景就沒辦法使用變數 a

貼心小提示

讀者若想要再更了解 Execution Context,除了上網搜尋外,筆者剛剛舉的 Execution Context 紀錄變數的機制,指的是 Execution Context 裡的 Variable Object 這個東西。

當呼叫一個函式時,一個執行背景會被建立起來;相對地,當函式執行結束時,該執行背景會被處理掉 —— 這樣的操作情形跟堆疊(Stack)很像,而且是每一次呼叫函式(Call)時都會 Push 一個新的執行背景,結束就會 Pop 掉該函式的執行背景,這就是 Call Stack。

貼心小提示

Stack Overflow 名稱的由來就是:如果很北爛地刻意亂寫一個,比如說一個遞迴函式,處理不好的話會一直呼叫到自己,每一次呼叫函式就等於將新的 Execution Context Push 到 Call Stack,直到電腦的記憶體爆掉,這就是令人感到開心歡樂的 Stack Overflow

// 呼叫此函式保證讓您感到歡樂!
function callsItself() {
  callsItself();
}

2. Event Table

另外,JavaScript 有提供一些 API,譬如 setTimeout 代表計時器到的時候會觸發的事件;或者是 DOM 元素的事件監聽機制(Event Listener)。

由於程式碼不可能遇到事件監聽的情況時就卡在那邊(俗稱 Blocking),因此必須要有東西負責記錄有哪些事件被註冊,這就是 Event Table。

這裡就有同步與非同步程式碼的關鍵差異出現的地方:如果程式碼是同步的狀況,就有可能出現 Blocking 的情形,而非同步程式碼則是避免程序卡住,先記錄下來,等事件觸發時才會執行其他動作。

因此,如果碰到運算資源龐大的情形,有時候採用非同步的程序,將運算龐大的過程隔離掉也是一種解決方法。

3. Event Queue

再來,一但事件被觸發了(但同時也有可能多種其他事件也會觸發),必須要有另一個東西負責幫事件進行列隊,具有列隊性質的資料結構是 Queue,而因為是專門幫事件進行列隊的動作,這就是 Event Queue。

4. Event Loop

最後,假設開始有事件被逐步觸發,並且列隊到 Event Queue 裡面,必須要有人負責將事件的觸發進行執行對應的程序的動作Event Loop 則扮演此角色。

但必須要注意的是:Event Loop 會等到 Call Stack 被清空時(也就是主程序都完成的時候)才會開始將 Event Queue 裡面的事件一一拔出來,執行事件對應的程序

重點 1. 同步 V.S. 非同步的概念

同步(Sync.)代表程式碼會按照順序一行一行執行。

非同步(Async.)則會避免程式碼卡住(Blocking),採取其他策略來執行程序。

在 JavaScript 裡,Web 相關的 API 會經由事件的註冊,紀錄在 Event Table 上。

如果事件被觸發時,就會將該事件排序在 Event Queue 內部。

等到主程序 Call Stack 內部都執行完畢清空時,Event Loop 會開始將 Event Queue 所列隊的事件按照順序進行輸出的動作。

由於本系列要講到泛用型別的應用,因此對於非同步的過程不在多作敘述。

以下筆者就要開始深論非同步程式碼語法在 JS 的演變(當然要結合 TypeScript XDDDD)。

非同步編程的演化三部曲 Evolution of Asynchronous Programming - The Trilogy

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

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

以上這一句話可以貫穿整個非同步編程在 JavaScript 領域的演變過程,一併地解釋 Async-Await 語法的演化由來,以下就由筆者娓娓道來。

混沌的開端 —— 回呼函式地獄 Callback Hell

JavaScript 裡面,使得程式碼變成巢狀地獄的混亂根源莫屬 Callback Hell(又名 Pyramid of Doom),筆者代入簡單的範例。

https://ithelp.ithome.com.tw/upload/images/20191015/201206146X91fzhLxA.png

以上的程式碼宣告一個很白癡的 sendRequest 來代表送出請求的功能。

假設想要達成送出三個請求,並且得依序送出,後面的請求必須等待前面請求完成才能開始執行。(以下的程式碼執行結果如圖一)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614I50tarwcpw.png
圖一:依序送出不同的 Request 結果,不過如果讀者 Run 以上的程式碼,有可能會中斷出現 500 Server Error 的原因是因為在 sendRequest 筆者刻意保留有出現這個狀況的可能

儘管功能是達到了 —— 想當然,這種寫法實在是麻煩甚至是爛透了 —— 巢狀部分光是幾個空白鍵要多少就很麻煩外,你會發現,假設今天要 Handle 第一個 Request 的錯誤結果,你得爬到最後面才能處理到。(如圖二)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614HWcfwDFfyi.png
圖二:錯誤要進行處理真是麻煩

由於需求是:必須等待前一個請求送出完畢後,才能送出後續的請求 —— 因此以這種回呼函式的寫法,後面的請求必須要寫在前面的請求的回呼函式裡,這樣造成請求之間的使用耦合度高,很難把功能拔出來

於是 —— Promise 物件就此誕生了。

首部曲 —— 攤平巢狀地獄的 Promise Chain

還記得前一篇講到的 Promise 物件的特性嗎?

筆者就再把那張連筆者覺得很精美的圖給附上來。(如圖三)

https://ithelp.ithome.com.tw/upload/images/20191015/201206149pNKCKUZYw.png
圖三:Promise 的狀態機示意圖

其中,昨天筆者有提到很重要的點:Promise 物件是可以被串接的,這個特性是一個絕佳極好的特性!

筆者就示範將剛剛的 sendRequest 改寫成 Promise 版本的物件:

https://ithelp.ithome.com.tw/upload/images/20191015/2012061415PWYOJWyy.png

恩~其實就只是把剛剛那一連串 successerror 的回呼函式改成用 Promise 提供的 resolvereject 來處理。如果要實踐相同的功能,也就是等待前一個請求送出完畢後,才能送出後續的請求 —— 首先,我們當然可以使用巢狀寫法來處理:

https://ithelp.ithome.com.tw/upload/images/20191015/20120614wZ9F6Vjz3y.png

但是,更好的解法是使用 Promise 物件如果回傳新的 Promise 物件時,可以進行串接的特性:

https://ithelp.ithome.com.tw/upload/images/20191015/20120614e8tJF3JuSf.png

你可以發現,原本的巢狀地獄就消失得無影無蹤了,圖四是執行結果。

https://ithelp.ithome.com.tw/upload/images/20191015/20120614Ok9ZBwTJkK.png
圖四:執行結果正常

但假設筆者偶然執行出現錯誤時,就算 catch 被串接到後面也可以正常運行。(如圖五)

https://ithelp.ithome.com.tw/upload/images/20191015/20120614fkZNlqm4zK.png
圖五:第一次送出請求是 200 狀態,但第二次失敗時,也可以執行到 catch

讀者試試看

其實 Promise 串接可以研究的東西多得很,讀者有興趣可以試試看各種不同的串接方式,但筆者選擇的是個人習慣的寫法,所以也沒什麼太多可以講的。

  1. 每組皆使用 then 然後 catch
    https://ithelp.ithome.com.tw/upload/images/20191015/201206144ZTz62cBwd.png

  2. 每組不使用 catch,但是都在 then 的第二個參數位置進行 catch

https://ithelp.ithome.com.tw/upload/images/20191015/20120614Jiphx2ZCRr.png

經過剛剛的講解,我們得知:

重點 2. Promise Chain

Promise Chain —— 也就是 Promise 物件的串接可以解決掉一個很麻煩的問題 —— 回呼函式的地獄 Callback Hell —— 可以使用 Promise Chain 進行攤平(Flatten)的動作。

非同步編程的演變過程,第一個環節就這樣結束了~

小結

由此可知,脫離巢狀地獄的第一步就是 —— 善用 Promise Chain 技巧,讓巢狀結構不覆存在。

不過呢,非同步的編寫手法還沒結束,下一篇筆者要介紹 ES6 Generators。

這段過程坦白說會非常麻煩,但理解過後會覺得有一種 —— 哦~~~ 的感覺。


上一篇
Day 47. 通用武裝・泛型應用 X 結合 ES2015+ - TypeScript Generics with ES2015+ Features
下一篇
Day 49. 通用武裝・非同步迭代 X 無窮地惰性求值 - TypeScript Generics with Asynchronous Programming II. ES6 Generators
系列文
讓 TypeScript 成為你全端開發的 ACE!50

尚未有邦友留言

立即登入留言