# Functional Reactive Programming
什麼是 Functional Reactive Programming?
我們先從幾個簡單的概念介紹,比如說我們修改了表單內的內容,我們希望改變的值能夠「即時」反映在 UI 上頭。
最直覺的做法就是在 input 當中監聽 input(或 change)事件,然後讓 UI 上的值改變。
input.addEventListener('change', e => {
price.textContent = e.target.value;
});
通常這叫做 pub-sub 的設計模式,這裏並不打算細談 pub-sub 的定義是什麼,不過或許可以從這個案例當中來思考,有沒有什麼優雅的方式來讓資料與 UI 整合在一起?
以剛剛的例子來說,我們透過 addEventListener
監聽 input,再將 input 的值綁定到 UI 上(或是資料上)。
這是一種很常見且有效的方式,對於 input 來說,只要乖乖地將事件吐出來就好,不需要知道其他事。
但在 UI 上,這樣的案例似乎過於理想。例如我們在實作打折的計算時,恐怕需要根據 1. 價格 2. 折數 來決定最終數字,這樣就需要在 UI 上加入兩個監聽器。
一旦來源變多了之後,恐怕會難以 debug,因為你不知道哪些 event listener 造成數據變化;而且 UI 上的變化可能依賴於不只一個事件。
input.addEventListener('change', e => {
price.textContent = e.target.value;
// update UI part B
// update UI part C
// ...
});
或者這個事件的發佈,不只會更新一個地方的 UI,也會讓數個 UI 更新,這時事件處理器就會變得比較複雜一些。
另外,在事件發生時,我們可能不只想要單純更新 UI 而已,例如按下按鈕時發送 ajax 拿取資料、對資料作轉換、錯誤處理等,最後才是更新 UI。
因此在處理上,有時會用另外一種方式 — data binding 來做處理。在 UI 產生的時候,就先將 UI 上的顯示與數據綁定在一起,在數據改變的時候,UI 做對應的處理。
# Reactive Programming
其實剛剛提到的 pub-sub,也就是加入事件監聽器的方式,跟 reactive programming 的概念有點像。就是在事件發生時,我們監聽他的變化,並且做出對應的改變。
再更深入一點談論,或許可以參考維基百科的定義:
reactive programming is a declarative programming paradigm (opens new window) concerned with data streams (opens new window) and the propagation of change. With this paradigm it is possible to express static (e.g., arrays) or dynamic (e.g., event emitters) data streams with ease, and also communicate that an inferred dependency within the associated execution model exists, which facilitates the automatic propagation of the changed data flow.
最經典的例子大概就是像試算表那樣的功能,每當值有變化的時候,對應的欄位就會一起改變。
The introduction to Reactive Programming you've been missing (opens new window) 這篇文章中將 UI 的互動當作一個無止盡的 stream。而 stream 的來源可能是滑鼠、時間、鍵盤操作、File IO、網路請求等等,經過一連串的處理之後,響應到 UI 上。
# 什麼是 funcional?
在 functional programming 裏頭雖然還有很多專有名詞,但可以簡單把 functional 定義為
輸入相同會造成相同輸出,沒有副作用的產生。
不過副作用到底是什麼?在這裡我們可以想成任何會讓輸出不固定(非預期)的行為,像是發送 network 請求(請求可能失敗、錯誤、超時)、File IO 等等。
但一個應用怎麼可能不發送 network?在這個情況下我們又要怎麼定義副作用呢?接下來就讓我們瞧瞧 RxJS。
# RxJS
RxJS 最初是由 Microsoft 開源,官方介紹如下:
The Reactive Extensions (Rx) is a library for composing asynchronous and event-based programs using observable sequences and LINQ-style query operators
因此在介紹 RxJS 前,我們先來瞧瞧 LINQ 是什麼。
# 什麼是 LINQ?
LINQ(Language Intergrated Query)是一套由程式語言定義的查詢。最初是由 Microsoft .NET Framework 在 2007 年推出。
在程式當中時常有需要查詢、整合、統計、過濾等對資料進行一連串操作的需求,但同時也會遇到一些問題:
- 資料來源不一,hard-coded 程式碼難以復用
- 下 SQL 的話在程式中難以除錯(因為都是字串),而且只有資料庫的資料才能被 SQL 查詢
所以微軟推出的這套技術,可以透過一連串的 operator 來操作資料。像是:
// string collection
IList<string> stringList = new List<string>() {
"C# Tutorials",
"VB.NET Tutorials",
"Learn C++",
"MVC Tutorials" ,
"Java"
};
// LINQ Query Syntax
var result = stringList.Where(s => s.Contains("Tutorials"));
範例自 LINQ Tutorial (opens new window)。
而這個概念被推廣後變成了 ReactiveX,除了 JavaScript 之外還有多個語言實作版本,像是 RxJava RxSwift 等等。
透過 RxJS,我們可以很方便地管理資料流。
# Observable
從前端的角度來想,其實 UI 的操作也有點像資料庫,我們查詢、過濾有興趣的事件(click, onChange 等),然後對事件作操作與轉換(click -> ajax call),最後反應到 UI 上。但整個過程並不一定是同步的,例如我們並不知道使用者何時會按下按鈕,何時會有 offline event,所以想要像陣列一樣可以透過各種 operator 來簡化操作的話,勢必就要一層抽象。
這一連串的操作可以抽象成 Observable (可被觀察),當有事件發生時,Observable 會作出對應的操作。像是在 onClick 當中,使用 RxJS 可以這麼寫:
Observable.fromEvent(document, 'click')
.filter(e => e.target === myButton)
.switchMap(() => ajax('/api/list').map(/* operation */))
乍看之下這個行為與程式碼變複雜了,但實際上我們可以將所有的資料來源(不管是事件、請求、單純的陣列等)用同一個介面操作,大幅簡化了針對特定型別處理的時間。
最經典的案例或許可以從 auto-complete 這個功能思考:
const input$ = Observable.fromEvent(input, 'input');
input$
.filter(e => e.target.value.length > 3) // 輸入大於 3 個字才開會繼續
.debounceTime(300) // debounce 300ms
.switchMap(e => ajax('/api/search?q=' + e.target.value) // 建立新的 Observable 發送 ajax 請求
.map(res => res.data) // map 成想要的資料
)
.retry(4) // 如果有錯誤會自動 retry 4 次
.subscribe(list => { // 實際 render UI
// render your data
})
這個 Observable 透過一連串的 operator 完成了幾件事:
- 接收 onInput 事件
- 過濾任何長度小於 3 的字串
- debounce 300 毫秒
- 轉換成另一個 Observable 發送請求。(在 rxjs 透過 switchMap 等 high order observable 可以幫你將 Observable 打平)
- 將回應 map 成想要的資料
- 如果有錯誤會 retry 4 次
subscribe
結果
你也可以自行定義 Observable,只要符合 Observable 的介面就可以用各種 operator 把玩!現在 RxJS 6 已經全部改用 pipe
的方式來組織 Observable,但概念一樣是相通的。
# redux-observable (opens new window)
由 Netflix 工程師開源的 redux-observable 近幾年受到熱烈迴響,透過 RxJS 的威力來管理 side-effect 是個相當優雅的做法,搭配 RxJS 的 operator 可以讓我們很容易管理這些複雜的資料流。如果覺得應用當中的 API 相當錯綜複雜,或是副作用總是令人厭煩的話,或許可以參考看看 redux-observable。
# 後記
在大概 2015 年的時候因為受到 denny 感召試用成主顧,蠻慶幸在剛入門前端的時候就能學習這麼受用的函式庫與概念。
這篇的內容其實蠻雜的,感覺沒辦法讓人感受到 RxJS 帶來的威力。但至少希望你能看完連結裡的內容。
另外這裡附上幾篇相當不錯的文章:
- Reactive Programming 簡介與教學(以 RxJS 為例) (opens new window)
- IT 鐵人賽 - 精通 RxJS (opens new window)
- 輕鬆應付複雜的非同步操作:RxJS Redux Observable (opens new window)
- redux-observable 起手式 (opens new window)
雖然 RxJS 在概念上比較難理解,也要花時間了解每個 operator 在做什麼,程式碼寫起來也不是那麼直覺,不過學習 RxJS(或是背後的概念)是前端蠻受用的知識,甚至可以說 CP 值超高!
如果想要了解更多,可以參考在當時也相當有名的文章 Your Mouse is a Database (opens new window),基本上將 Reactive Programming 與 RxJS 的概念與想要解決的問題解釋得很詳盡。真希望有人可以幫忙翻譯成中文啊...。