RP(Reactive Programming)入門
什么是RP?
在互聯網上有著一大堆糟糕的解釋與定義。維基百科一如既往的空泛與理論化。Stackoverflow的權威答案明顯不適合初學者。Reactive Manifesto看起來是你展示給你公司的項目經理或者老板們看的東西。微軟的Rx terminology "Rx = Observables + LINQ + Schedulers" 過于重量級且微軟味十足,只會讓大部分人困惑。相對于你所使用的MV*框架以及鐘愛的編程語言,"Reactive"和"Propagation of change"這些術語并沒有傳達任何有意義的概念。框架的Views層當然要對Models層作出反應,改變當然會傳播(分別對應上文 的"Reactive"與"Propagation of change",意思是這一大堆術語和廢話差不多,翻譯不好,只能靠備注了)。如果沒有這些,就沒有東西會被渲染了。
所以不要再扯這些廢話了。
RP是使用異步數據流進行編程
一方面,這并不是什么新東西。Event buses或者Click events本質上就是異步事件流(Asynchronous event stream),你可以監聽并處理這些事件。RP的思路大概如下:你可以用包括Click和Hover事件在內的任何東西創建Data stream(原文:"FRP is that idea on steroids. You are able to create data streams of anything, not just from click and hover events.")。Stream廉價且常見,任何東西都可以是一個Stream:變量、用戶輸入、屬性、Cache、數據結構等等。舉個例子,想像一下 你的推ter feed就像是Click events那樣的Data stream,你可以監聽它并相應的作出響應。
在這個基礎上,你還有令人驚艷的函數去combine、create、filter這些Stream。這就是函數式(Functional)魔法的用武之地。Stream能接受一個,甚至多個Stream為輸入。你可以merge兩個Stream,也可以從一個Stream中filter出你感興趣的Events以生成一個新的Stream,還可以把一個Stream中的Data values map到一個新的Stream中。
既然Stream在RP中如此重要,那么我們就應該好好的了解它們,就從我們熟悉的"Clicks on a button" Event stream開始。
Stream就是一個 按時間排序的Events(Ongoing events ordered in time)序列 ,它可以emit三種不同的Events:(某種類型的)Value、Error或者一個"Completed" Signal。考慮一下"Completed"發生的時機,例如,當包含這個Button(指上面Clicks on a button"例子中的Button)的Window或者View被關閉時。
通過分別為Value、Error、"Completed"定義事件處理函數,我們將會異步地捕獲這些Events。有時可以忽略Error與"Completed",你只需要定義Value的事件處理函數就行。監聽一個Stream也被稱作是 訂閱(Subscribing),而我們所定義的函數就是觀察者(Observer),Stream則是被觀察者(Observable),其實就是觀察者模式(Observer Design Pattern)。
上面的示意圖也可以使用ASCII重畫為下圖,在下面的部分教程中我們會使用這幅圖:
--a---b-c---d---X---|->
a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline
既然已經開始對RP感到熟悉,為了不讓你覺得無聊,我們可以嘗試做一些新東西:我們將會把一個Click event stream轉為新的Click event stream。
首先,讓我們做一個能記錄一個按鈕點擊了多少次的計數器Stream。在常見的RP庫中,每個Stream都會有多個方法,map
、filter
、scan
等等。當你調用其中一個方法時,例如clickStream.map(f)
,它就會基于原來的Click stream返回一個 新的Stream。它不會對原來的Click steam作任何修改。這個特性就是 不可變性(Immutability),它之于RP Stream,就如果汁之于薄煎餅。我們也可以對方法進行鏈式調用如clickStream.map(f).scan(g)
:
clickStream: ---c----c--c----c------c-->
vvvvv map(c becomes 1) vvvv
---1----1--1----1------1-->
vvvvvvvvv scan(+) vvvvvvvvv
counterStream: ---1----2--3----4------5-->
map(f)
會根據你提供的f
函數把原Stream中的Value分別映射到新的Stream中。在我們的例子中,我們把每一次Click都映射為數字1。scan(g)
會根據你提供的g
函數把Stream中的所有Value聚合成一個Value -- x = g(accumulated, current)
,這個示例中g
只是一個簡單的add函數。然后,每Click一次,counterStream
就會把點擊的總次數發給它的觀察者。
為了展示RP真正的實力,讓我們假設你想得到一個包含雙擊事件的Stream。為了讓它更加有趣,假設我們想要的這個Stream要同時考慮三擊 (Triple clicks),或者更加寬泛,連擊(Multiple clicks)。深呼吸一下,然后想像一下在傳統的命令式且帶狀態的方式中你會怎么實現。我敢打賭代碼會像一堆亂麻,并且會使用一些的變量保存狀態,同時 也有一些計算時間間隔的代碼。
而在RP中,這個功能的實現就非常簡單。事實上,這邏輯只有4行代碼。但現在我們先不管那些代碼。用圖表的方式思考是理解怎樣構建Stream的最好方法,無論你是初學者還是專家。
灰色的方框是用來轉換Stream的函數。首先,我們把連續250ms內的Click都放進一個列表(原文:"First we accumulate clicks in lists, whenever 250 milliseconds of "event silence" has happened." 實在不知道怎么翻譯,就按自己的理解寫了一下) -- 簡單來說就是buffer(stream.throttle(250ms))
做的事,不要在意這些細節,我們只是展示一下RP而已。結果是一個列表的Stream,然后我們使用map()
把每個列表映射為一個整數,即它的長度。最終,我們使用filter(x >= 2)
把整數1
給過濾掉。就這樣,3個操作就生成了我們想要的Stream。然后我們就可以訂閱(監聽)這個Stream,并以我們所希望的方式作出反應。
我希望你能感受到這個示例的優美之處。這個示例只是冰山一角:你可以把同樣的操作應用到不同種類的Stream上,例如,一個API響應的Stream;另一方面,還有很多其它可用的函數。
為什么我要使用RP
RP提高了代碼的抽象層級,所以你可以只關注定義了業務邏輯的那些相互依賴的事件,而非糾纏于大量的實現細節。RP的代碼往往會更加簡明。
特別是在開發現在這些有著大量與Data events相關的UI events的高互動性Webapps、Mobile apps的時候,RP的優勢將更加明顯。10年前,網頁的交互就只是提交一個很長的表單到后端,而前端只有簡單的渲染。Apps就表現得更加的實時了:修 改一個表單域就能自動地把修改后的值保存到后端,為一些內容"點贊"時,會實時的反應到其它在線用戶那里等等。
現在的Apps有著大量各種各樣的實時Events,以給用戶提供一個交互性較高的體驗。我們需要工具去應對這個變化,而RP就是一個答案。
以RP方式思考的例子
讓我們做一些實踐。一個真實的例子一步一步的指導我們以RP的方式思考。不是虛構的例子,也沒有只解釋了一半的概念。學完教程之后,我們將寫出真實可用的代碼,并做到知其然,知其所以然。
在這個教程中,我將會使用 JavaScript 和 RxJS,因為JavaScript是現在最多人會的語言,而Rx* 庫有多種語言版本,并支持多種平臺(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy, 等等)。所以,無論你用的是什么語言、庫,你都能從下面這個教程中學到東西。
實現"Who to follow"推薦界面
在推ter上,這個界面看起來是這樣的:
我們將會重點模擬它的核心功能,如下:
- 啟動時從API那里加載帳戶數據,并顯示3個推薦
- 點擊"Refresh"時,加載另外3個推薦用戶到這三行中
- 點擊帳號所在行的'x'按鈕時,清除那個推薦然后顯示一個新的推薦
- 每行都會顯示帳號的頭像,以及他們主頁的鏈接
我們可以忽略其它的特性和按鈕,因為它們是次要的。同時,因為推ter最近關閉了對非授權用戶的API,我們將會為Github實現這個推薦界面,而非推ter。這是Github獲取用戶的API。
如果你想先看一下最終效果,這里有完成后的代碼 http://jsfiddle.net/staltz/8jFJH/48/ 。
https://github.com/benjycui/introrx-chinese-edition