認識JavaScript的Callback,Promise,RxJS,async/await
今天我要來介紹JavaScript時常被拿來討論的非同步問題,首先我們要來先討論,為什麼我們需要在JavaScript處理非同步問題。
為什麼需要處理非同步問題
在開發專案的時候,當你設計好一個Http request時,你可能會遇到神奇的狀況。
例如:
- JSON server的資料未更新
- 後端未收到資料衍生的error
- 後端收到的資料,是未經過處理
- 已經寫了修改資料的程式,卻沒有任何動靜
舉例
這些舉例的問題,通常是初學時常碰到的,明明邏輯都沒問題,卻依然無法讓程式順利進行。
如果你未經歷過,無法共鳴上述的情境的話,以下再提供一段程式讓各位品嚐。
你覺得以下程式碼會執行出什麼樣的結果?:
1 | function doSomething() { |
你會發現他拋出了以下錯誤:
1 | error: Uncaught TypeError: Cannot read property 'name' of undefined |
這時,你就會體會到上面講的情境了,明明邏輯都沒問題,卻依然無法讓程式順利進行的狀況產生。
要知道為什麼,你需要先了解JavaScript是怎麼執行程式的。
原因
JavaScript是一個單執行緒的程式語言,因此無法做到多工的效果。
你以為執行順序應該是:
- 執行getUser()
- setTimeout執行2秒
- assign getUser() to user
- 輸出user
事實上是:
- 執行getUser()
- 輸出user(ERROR發生!!)
- setTimeout執行2秒
- assign getUser() to user
JavaScript執行同一層的function scope,會依序把任務放入CallStack。
需要等待執行的任務(setTimeout),會放入瀏覽器的APIs進行setTimeout的執行。
執行完setTimeout的程式會放入Message Queue(Task Queue),再從Message Queue取得任務(執行完setTimeout的任務)
再來Event Loop會等待與監聽任務的執行,並且依序將function scope拆解出來放進CallStack。
以下再來給一個範例:
1 | const sayHi = () => { |
你預期要先alert再log,但是經由上面的解釋之後,會是先log再alert,你看出來了嗎?
如何解決?
那我們要如何解決這種執行順序的問題呢?
答案就是Callback function。
Callbacks
Callback看似不容易理解,但是事實上原理很簡單,你可以理解成:如果這個事件發生了,麻煩幫我執行這個事件。
什麼意思呢?讓我們來看看以下範例:
1 | function doSomething() { |
這個程式碼是一開始的改良版,原本console.log是在getUser的裡面
但是現在放到getUser裡面變成Anonymous function傳進去,為什麼要這樣做呢?
剛剛有提到JavaScript是一個單執行緒的程式語言,所以我如果要等待某個特定的任務執行完,再執行下個任務時
我可以把我想要之後執行的任務包裝成一個Anonymous function傳進去,並且在適當的時機使用它,我就能達到非同步的效果。
所以執行順序就會變成:
- 執行getUser(…)
- 執行非同步 setTimeout執行2秒
- 執行callback function
- 輸出user
是不是比較容易理解了呢?
Welcome to the Callback Hell
但是事情有時候並不是這麼的簡單,歡迎來到Callback Hell。
以下是一個簡單的加法範例:
1 | function add (a, b, callback) { |
你有看出什麼問題嗎?
一般邏輯簡單的非同步也許可以用callback,但是當邏輯變複雜或是重複的事情變多時,callback就會越寫越精彩。
最後就變成大家喜歡惡搞的脈衝波圖片了。程式碼可想而知,也不好維護它了。
尤其在Error handling的時候,還要去思考callback callback了哪個callback又callback哪個callback……
這絕對不是你想要維護的程式碼吧…一點都不好玩!!
那該怎麼改善呢?JavaScript在ES6提出了一個Solution,Promise是我們的答案。
Promises
Promise就是字面上本身的意思。
JavaScript在ES6推出了Promise讓我們更方便去處理非同步。
在處理程式如果遇到非同步的問題,只要把希望之後再執行的程式包裝成Anonymous function放到then後面
然後再到程式裡面return 一個Promise去執行你放到then裡面的Anonymous function,就能達到我們要的目的。
以下是範例程式:
1 | const getUser = () => { |
Promise也提供兩個方便的參數:resolve 和 reject
當你呼叫resolve(data),表示你視同這個Promise執行成功,並且回傳參數到then。
而如果你呼叫reject(err),表示你認為這個執行有問題,需要拋出異常。
以下是我改良上面的Callback Hell變成Promise版本的程式碼:
1 | function add (a, b) { |
看完之後,你應該就會喜歡上用Promise了吧?而且程式碼更簡潔易懂。
當你有多筆任務的時候,你只需要不斷的then之後再then之後再then…即可。
不必再陷入多重callback的險境之中,更容易維護程式碼。
是不是和之前的程式碼差很多呢?
但是如果我們需要傳輸更多資料呢?
Promise有個小問題是,一次只能處理一次的資料。
這也是為什麼Observable會變得如此熱議的原因。
RxJS能解決我們的問題。
RxJS
RxJS提供Observable,用法不同於Promise。
Promise傳送一個Http request,server回覆response,並且解決Promise,最後結束程式。
但是RxJS並非如此,你可以handle streams of data,去監聽所有新的事件並且送出新的值,這是Promise無法辦到的事情。
詳細的內容可參考RxJS官方文件的Observable
分類 | Single | Multiple |
---|---|---|
Pull | Function | Iterator |
Push | Promise | Observable |
基本用法
RxJS不是本篇的重點,所以以下放個簡單的使用方式:
1 | const button = document.querySelector('button') |
RxJS使用方法不同於Promise,需使用Observable監聽,並再在透過subscribe去處理成功與失敗的問題。
等同於then的用法
你可以在subscribe裡面再放入一個Anonymous function。
1 | const button = document.querySelector('button'); |
這個時候你會不會心想:這不就又回來Callback Hell了嗎?
以下可以達到類似then的用法:
1 | const button = document.querySelector('button') |
透過switchMap將非同步的程式碼串在一起。
補充
由於RxJS6已經釋出了,所以使用上也比以往方便許多
現在你可以這樣宣告了:1
2
3import { Observable, Subject } from 'rxjs'
import { map, take } from 'rxjs/operators'
import { of } from 'rxjs'但是請注意!並不是Promise不好的意思,如果不需要處理大量的資料,那麼RxJS並非是一個好選擇。
Async/Await
當你在開發Promise和Observable的時候,相信你一定也會產生一點murmur。
為什麼我們要在這麼多blocks上寫程式?難道不能像以往一樣直接順著寫程式碼嗎?
那麼Async/Await可能會是你的選擇。
JavaScript在ES8推出了Async/Await給大家使用。
開發者只需要在function之前加入async,裡面的非同步函數加上await,即可輕鬆開發。
以下是範例程式:
1 | const button = document.querySelector("button"); |
只需要在想要達到非同步的程式碼加入,非常的方便。
結論
以上介紹這麼多東西,不管是哪一項技術,關鍵都在於你會不會用到它。
如果不需要用到,那麼挑選自己方便的技術只能即可。
除了開發好程式,也要注意是否影響到Development Experience。