我在工做中採用Reactive Programming(RP)已經有一年了,對於這個「新鮮」的辭藻或許有一些人還不甚熟悉,這裏就和你們說說關於RP個人理解。但願在讀完本文後,你可以用Reactive Extension進行RP。html
須要說明的是,我實在不知道如何翻譯Reactive Programming這個詞組,因此在本文中均用RP代替,而不是什麼「響應式編程」、「反應式編程」。本文假定你對JavaScript及HTML5有初步的瞭解,若是有使用過,那麼就再好不過了。前端
讓咱們首先來想象一個很常見的交互場景。當用戶點擊一個頁面上的按鈕,程序開始在後臺執行一些工做(例如從網絡獲取數據)。在獲取數據期間,按鈕不能再被點擊,而會顯示成灰色的」disabled」狀態。當加載完成後,頁面展示數據,然後按鈕又能夠再次使用。(以下面例子的這個load按鈕)react
在這裏我使用jQuery編寫了按鈕的邏輯,具體的代碼是這樣的。git
1 |
var loading = false; |
對應的HTML:github
1 |
<button class="load">Load</button> |
不知道你有沒有注意到,在這裏loading
變量實際上是徹底能夠不用存在的。而我寫出loading
變量,就是爲了抓住你的眼球。loading
表明的是一個狀態,意思是「個人程序如今有沒有在後臺加載程序」。ajax
另外還有幾個不是很明顯的狀態。好比按鈕的disabled
狀態(由$btn.prop('disabled')
得到),以及按鈕的文字。在加載的時候,也就是loading === true
的時候,按鈕的disable
狀態會是true
,而文字會是Loading ...
;在不加載的時候,loading === false
成立,按鈕的disabled
狀態就應該爲false
,而文字就是Load
。算法
如今讓咱們用靜態的圖來描述用戶點擊一次按鈕的過程。編程

若是用戶點擊不少次的按鈕的話,那麼loading
的值的變化將是這樣的。json
1 |
loading: false -> true -> false -> true -> false -> true -> ... |
相似像loading
這樣的狀態(state)在應用程序中隨處可見,並且其值的變化能夠不侷限於兩個值。舉個栗子,假如咱們如今設計微博的前端,一條微博的JSON數據形式以下:設計模式
1 |
var aWeibo = { |
另外有一個weiboList
數組,存儲當前用戶所看到的微博。
1 |
var weiboList = [ |
這固然是個極度精簡的模型了,真實的微博應用必定比這個複雜許多。可是有一個和loading
狀態很相似的就是weiboList
,由於咱們都知道每過一段時間微博就會自動刷新,也就是說weiboList
也在一直經歷着變化。
1 |
weiboList: [一些微博] -> [舊的微博,和一些新的微博] -> [更多的微博] -> ... |
再次強調,不管是weiboList
仍是loading
,它們都是應用程序的狀態。上面的用箭頭組成的示意圖僅僅是咱們對狀態變化的一種展示形式(或者說建模)。然而,咱們其實還能夠用更加簡單的模型來表現它,而這個模型咱們都熟悉 —— 數組。
若是它們都只是數組
若是說loading
變化的過程就是一個數組,那麼不妨把它寫做:
1 |
var loadingProcess = [false, true, false, true, false, ...] |
爲了表現出這是一個過程,咱們將其從新命名爲loadingProcess
。不過它沒有什麼不一樣,它是一個數組。並且咱們還能夠注意到,按鈕的disabled
狀態的變化過程和loadingProcess
的變化過程是如出一轍的。咱們將disabled
的變化過程命名爲disabledProcess
。
1 |
var disabledProcess = [false, true, false, true, false, ...] |
那麼若是將loadingProcess
作下面的處理,咱們將獲得什麼呢?
1 |
var textProcess = loadingProcess.map(function(loading) { |
咱們獲得的將是按鈕上文字的狀態變化過程,也就是$btn.text()
的值。咱們將其命名爲textProcess
。在有了textProcess
和disabledProcess
以後,就能夠直接對UI進行更新。在這裏,咱們再也不須要使用到loadingProcess
了。
1 |
disabledProcess.forEach(function (disabled) { |
這個變換的過程看起來就像下圖。

在YY了那麼久以後,你可能會說,不對啊!狀態的變化是一段時間內發生的事情,在程序一開始怎麼可能就知道以後的所有狀態,並所有放到一個數組裏面呢?是的,咱們在以前刻意省略掉了一個重要的元素,也就是時間(time)。
時間都去哪兒啦?
loadingProcess
是如何得出的?當用戶觸發按鈕的點擊事件的時候,loadingProcess
會被置爲false
;而當HTTP請求完成的時候,咱們將其置爲true
。在這裏,用戶觸發點擊事件,和HTTP請求完成都是一個須要時間的過程。用戶的兩次點擊之間一定要有時間,就像這樣:
clickEvent … clickEvent …… clickEvent ….. clickEvent
兩個clickEvent之間一個點咱們假設表明一秒鐘,用戶點擊的事件之間是由長度不一樣的時間間隔開的。
若是咱們再嘗試用剛纔的方法,把click事件表示成一個數組,就會以爲特別的古怪:
1 |
var clickEventProcess = |
你會想,古怪之處在於,這裏沒了時間的概念。其實不必定是這樣的。你以爲這裏少了時間,只是由於你被我剛纔的例子所迷惑了。你的腦殼裏面多是在想下面的這段代碼:
1 |
// 代碼A |
若是是下面這段代碼,我相信你再熟悉不過了,你還會以爲奇怪嗎?
1 |
// 代碼B |
代碼A中,咱們所看到的是迭代器模式(Iterative Pattern)。所謂迭代器模式是對遍歷一個集合的算法所進行的抽象。對於一個數組、一個二叉樹和一個鏈表的遍歷算法各不相同,但我均可以用統一的一個接口來獲取遍歷的結果。forEach
就是一個例子。
1 |
數組.forEach(function (元素) { /* ... */}); |
雖然每一個forEach
的實現方式必定不一樣,可是隻要接口(即forEach
這個名字以及元素
這個參數)一致,我就能夠遍歷它們之中任何的一個,無論是數組、二叉樹仍是二郎神。只要它們都是實現了forEach
的集合。
下面這句話但願你仔細品味:
迭代器模式的一個最大的特色就是,數據是由你向集合索要過來的。
在使用迭代器的時候,咱們其實就是在向集合要數據,並且每次都企圖一次性要完。
1 |
[1,2,3,4,5].forEach(function (num) { |
這就好像在對集合說,你把那五個數字給我吧,快點兒,一個接一個一次性給完。在生活中,就好像蛋糕店的服務員幫你切蛋糕同樣。你老是在和服務員說,麻煩你再給我下一塊,再給我下一塊……

而代碼B是截然相反的。在代碼B中,咱們是在等待着數據被推送過來。又拿切蛋糕爲例,此次就好像是你一聲不響,而服務員一直跟你說,「這塊切好了,給你!」。

若是你對設計模式熟悉的話,你應該知道代碼B的模式叫作觀察者模式(Observer Pattern)。所謂觀察者模式,就是你觀察集合,當集合告訴你它有元素要給你的時候,你就能夠拿到元素。addEventListener
自己就是一個很好的觀察者模式的例子。
在切蛋糕的例子中,當你雙目注視的服務員,耳朵豎得高高的,你就是在對服務員進行觀察。每當服務員告訴你,有一塊新的蛋糕切好了,你就過去拿。
迭代器和觀察者的對立和統一
迭代器模式和觀察者模式本質上是對稱的。它們相同的地方在於:
- 都是對集合的遍歷(都是那塊大蛋糕)
- 每次都只得到一個元素
他們徹底相反的地方只有一個:迭代器模式是你主動去要數據,而觀察者模式是數據的提供方(切蛋糕的服務員)把數據推給你。他們其實徹底能夠用一樣的接口來實現,例如前面的例子中的代碼A,咱們來回顧一下:
1 |
// 代碼A |
對於代碼B,咱們能夠進行以下的改寫
1 |
// 代碼B |
咱們解讀一下修改過的代碼B。
clickEventProcess.forEach
: 它接受一個回調函數做爲參數,並存儲在this._fn
裏面。這是爲了未來在clickEventProcess.onNext
裏面調用- 當clickEvent觸發的時候,調用
clickEventProcess.onNext(clickEvent)
,將clickEvent
傳給了clickEventProcess
clickEventProcess.onNext
將clickEvent
傳給了this._fn
,也就是以前咱們所存儲的回調函數- 回調函數正確地接收到新的點擊事件
來看看如今發生了什麼……迭代器模式和觀察者模式用了一樣的接口(API)實現了!由於,它們本質上就是對稱的,能用一樣的API將兩件本來對稱的事物給統一塊兒來,這是能夠作到的。
迭代器模式,英文叫作Iterative,由你去迭代數據;而觀察者模式,要求你對數據來源的事件作出反應(react),因此其實也能夠稱做是Reactive(能作出反應的)。Iterative和Reactive,互相對稱,相愛不相殺。
話外音:在這裏我沒有明確說起,實際上在觀察者模式中數據就是以流(stream)的形式出現。而所謂數組,不過就是無需等待,立刻就能夠得到全部元素的流而已。從流的角度來理解Iterative和Reactive的對稱性也能夠,這裏咱們很少加闡述。
Reactive Extension
上面代碼B中咱們最後得到了一個新的clickEventProcess
,它不是一個真正意義上的集合,卻被咱們抽象成了一個集合,一個被時間所間隔開的集合。 Rx.js,也稱做Reactive Extension提供給了抽象出這樣集合的能力,它把這種集合命名爲Observable
(可觀察的)。
添加Rx.js及其插件Rx-DOM.js。咱們須要Rx-DOM.js,由於它提供網絡通信相關的Observable抽象,稍後咱們就會看到。
1 |
<script src="https://cdn.rawgit.com/Reactive-Extensions/RxJS/master/dist/rx.all.min.js"></script> |
只須要很簡單的一句工廠函數(factory method)就能夠將鼠標點擊的事件抽象成一個Observable
。Rx.js提供一個全局對象Rx
,Rx.Observable
就是Observable的類。
1 |
var loadButton = document.querySelector('.load'); |
click$
就是前面的clickEventProcess
,在這裏咱們將全部的Observable變量名結尾都添加$
。點擊事件是像下面這樣子的:
1 |
[click ... click ........ click .. click ..... click ..........] |
每一個點擊事件後應該發起一個網絡請求。
1 |
var response$$ = click$.map(function () { |
Rx.DOM.ajax.get
會發起HTTP GET請求,並返回響應(Response)的Observable。由於每次請求只會有一個響應,因此響應的Observable實際上只會有一個元素。它將會是這樣的:
1 |
[...[.....response].......[........response]......[....response]...........[....response]......[....response]] |
因爲這是Observable的Observable,就好像二維數組同樣,因此在變量名末尾是$$
。 若將click$和response$$的對應關係勾勒出來,會更加清晰。

然而,咱們更但願的是直接得到Response的Observble,而不是Response的Observble的Observble。Rx.js提供了.flatMap
方法,能夠將二維的Observable「攤平」成一維。你能夠參考underscore.js裏面的flatten
方法,只不過它是將普通數組攤平,而非將Observable攤平。
1 |
var response$ = click$.flatMap(function () { |
圖示:

對於每個click事件,咱們都想將loading
置爲true
;而對於每次HTTP請求返回,則置爲false
。因而,咱們能夠將click$
映射成一個純粹的只含有true
的Observable,但其每一個true
到達的事件都和點擊事件到達的時間同樣;對於response$
,一樣,將其映射呈只含有false
的Observable。最後,咱們將兩個Observable結合在一塊兒(用Rx.Observable.merge
),最終就能夠造成loading$
,也就是剛纔咱們的loadingProcess
。
此外,$loading
還應有一個初始值,能夠用startWith
方法來指定。
1 |
var loading$ = Rx.Observable.merge( |
整個結合的過程如圖所示

有了loading$
以後,咱們很快就能得出剛纔咱們所想要的textProcess
和enabledProcess
。enabledProcess
和loading$
是一致的,就無需再生成,只要生成textProcess
便可(命名爲text$
)。
1 |
var text$ = loading$.map(function (loading) { |
在Rx.js中沒有forEach
方法,但有一個更好名字的方法,和forEach
效用同樣,叫作subscribe
。這樣咱們就能夠更新按鈕的樣式了。
1 |
|
這樣就用徹底Reactive的方式重構了以前咱們的例子。
在咱們重構後的方案中,消滅了全部的狀態。狀態都被Observable抽象了出去。因而,這樣的代碼若是放在一個函數裏面,這個函數將是沒有反作用的純函數。關於純函數、函數式編程,能夠閱讀個人文章《「函數是一等公民」背後的含義》。
總結
本文從應用的角度入手解釋了Reactive Programming的思路。Observable做爲對狀態的抽象,統一了Iterative和Reactive,淡化了二者之間的邊界。固然,最大的好處就是咱們用抽象的形式將煩人的狀態趕出了視野,取而代之的是可組合的、可變換的Observable。
事物之間的對立統一一般很難找到。實際上,即便是在《設計模式》這本書中,做者們也不曾看到迭代器模式和觀察者模式之間存在的對稱關係。在UI設計領域,咱們更多地和用戶驅動、通訊驅動出來的事件打交道,這才促成了這兩個模式的合併。