我在工做中採用Reactive Programming(RP)已經有一年了,對於這個「新鮮」的辭藻或許有一些人還不甚熟悉,這裏就和你們說說關於RP個人理解。但願在讀完本文後,你可以用Reactive Extension進行RP。html

須要說明的是,我實在不知道如何翻譯Reactive Programming這個詞組,因此在本文中均用RP代替,而不是什麼「響應式編程」、「反應式編程」。本文假定你對JavaScript及HTML5有初步的瞭解,若是有使用過,那麼就再好不過了。前端

讓咱們首先來想象一個很常見的交互場景。當用戶點擊一個頁面上的按鈕,程序開始在後臺執行一些工做(例如從網絡獲取數據)。在獲取數據期間,按鈕不能再被點擊,而會顯示成灰色的」disabled」狀態。當加載完成後,頁面展示數據,然後按鈕又能夠再次使用。(以下面例子的這個load按鈕)react

在這裏我使用jQuery編寫了按鈕的邏輯,具體的代碼是這樣的。git

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var loading = false;

$('.load').click(function () {
loading = true;

var $btn = $(this);

$btn.prop('disabled', loading);
$btn.text('Loading ...');

$.getJSON('https://www.reddit.com/r/cats.json')
.done(function (data) {
loading = false;
$btn.prop('disabled', loading);
$btn.text('Load');

$('#result').text("Got " + data.data.children.length + " results");
});
});

對應的HTML:github

1
2
<button class="load">Load</button>
<div id="result"></div>

不知道你有沒有注意到,在這裏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
2
3
4
var aWeibo = {
user: 1,
text: '我今天好高興啊!'
};

另外有一個weiboList數組,存儲當前用戶所看到的微博。

1
2
3
4
5
var weiboList = [
{user: 1, text: '今天又出去玩了'},
{user: 2, text: '人有多大膽,地有多大產!'},
// ...
]

這固然是個極度精簡的模型了,真實的微博應用必定比這個複雜許多。可是有一個和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
2
3
var textProcess = loadingProcess.map(function(loading) {
return loading ? "Loading ..." : "Load"
});

咱們獲得的將是按鈕上文字的狀態變化過程,也就是$btn.text()的值。咱們將其命名爲textProcess。在有了textProcessdisabledProcess以後,就能夠直接對UI進行更新。在這裏,咱們再也不須要使用到loadingProcess了。

1
2
3
4
5
6
disabledProcess.forEach(function (disabled) {
$btn.prop('disabled', disabled);
});
textProcess.forEach(function (text) {
$btn.text(text);
});

這個變換的過程看起來就像下圖。

在YY了那麼久以後,你可能會說,不對啊!狀態的變化是一段時間內發生的事情,在程序一開始怎麼可能就知道以後的所有狀態,並所有放到一個數組裏面呢?是的,咱們在以前刻意省略掉了一個重要的元素,也就是時間(time)

時間都去哪兒啦?

loadingProcess是如何得出的?當用戶觸發按鈕的點擊事件的時候,loadingProcess會被置爲false;而當HTTP請求完成的時候,咱們將其置爲true。在這裏,用戶觸發點擊事件,和HTTP請求完成都是一個須要時間的過程。用戶的兩次點擊之間一定要有時間,就像這樣:

clickEvent … clickEvent …… clickEvent ….. clickEvent

兩個clickEvent之間一個點咱們假設表明一秒鐘,用戶點擊的事件之間是由長度不一樣的時間間隔開的。

若是咱們再嘗試用剛纔的方法,把click事件表示成一個數組,就會以爲特別的古怪:

1
var clickEventProcess = [ clickEvent, clickEvent, clickEvent, clickEvent, clickEvent, ... ]

你會想,古怪之處在於,這裏沒了時間的概念。其實不必定是這樣的。你以爲這裏少了時間,只是由於你被我剛纔的例子所迷惑了。你的腦殼裏面多是在想下面的這段代碼:

1
2
3
4
// 代碼A
clickEventProcess.forEach(function (clickEvent) {
// ...
});

若是是下面這段代碼,我相信你再熟悉不過了,你還會以爲奇怪嗎?

1
2
3
4
// 代碼B
document.querySelector('.load').addEventListener('click', function (clickEvent) {
// ...
});

代碼A中,咱們所看到的是迭代器模式(Iterative Pattern)。所謂迭代器模式是對遍歷一個集合的算法所進行的抽象。對於一個數組、一個二叉樹和一個鏈表的遍歷算法各不相同,但我均可以用統一的一個接口來獲取遍歷的結果。forEach就是一個例子。

1
2
3
數組.forEach(function (元素) { /* ... */});
二叉樹.forEach(function (元素) { /* ... */});
鏈表.forEach(function (元素) { /* ... */});

雖然每一個forEach的實現方式必定不一樣,可是隻要接口(即forEach這個名字以及元素這個參數)一致,我就能夠遍歷它們之中任何的一個,無論是數組、二叉樹仍是二郎神。只要它們都是實現了forEach的集合。

下面這句話但願你仔細品味:

迭代器模式的一個最大的特色就是,數據是由你向集合索要過來的。

在使用迭代器的時候,咱們其實就是在向集合要數據,並且每次都企圖一次性要完。

1
2
3
[1,2,3,4,5].forEach(function (num) {
console.log(num);
});

這就好像在對集合說,你把那五個數字給我吧,快點兒,一個接一個一次性給完。在生活中,就好像蛋糕店的服務員幫你切蛋糕同樣。你老是在和服務員說,麻煩你再給我下一塊,再給我下一塊……

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

若是你對設計模式熟悉的話,你應該知道代碼B的模式叫作觀察者模式(Observer Pattern)。所謂觀察者模式,就是你觀察集合,當集合告訴你它有元素要給你的時候,你就能夠拿到元素。addEventListener自己就是一個很好的觀察者模式的例子。

在切蛋糕的例子中,當你雙目注視的服務員,耳朵豎得高高的,你就是在對服務員進行觀察。每當服務員告訴你,有一塊新的蛋糕切好了,你就過去拿。

迭代器和觀察者的對立和統一

迭代器模式和觀察者模式本質上是對稱的。它們相同的地方在於:

  1. 都是對集合的遍歷(都是那塊大蛋糕)
  2. 每次都只得到一個元素

他們徹底相反的地方只有一個:迭代器模式是你主動去要數據,而觀察者模式是數據的提供方(切蛋糕的服務員)把數據推給你。他們其實徹底能夠用一樣的接口來實現,例如前面的例子中的代碼A,咱們來回顧一下:

1
2
3
4
// 代碼A
clickEventProcess.forEach(function (clickEvent) {
// ...
});

對於代碼B,咱們能夠進行以下的改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 代碼B
clickEventProcess.forEach = function(fn) {
this._fn = fn;
};

clickEventProcess.onNext = function(clickEvent) {
this._fn(clickEvent);
};

document.querySelector('.load').addEventListener('click', function (clickEvent) {
clickEventProcess.onNext(clickEvent);
});

clickEventProcess.forEach(function (clickEvent) {
// ...
});

咱們解讀一下修改過的代碼B。

  1. clickEventProcess.forEach: 它接受一個回調函數做爲參數,並存儲在this._fn裏面。這是爲了未來在clickEventProcess.onNext裏面調用
  2. 當clickEvent觸發的時候,調用clickEventProcess.onNext(clickEvent),將clickEvent傳給了clickEventProcess
  3. clickEventProcess.onNextclickEvent傳給了this._fn,也就是以前咱們所存儲的回調函數
  4. 回調函數正確地接收到新的點擊事件

來看看如今發生了什麼……迭代器模式和觀察者模式用了一樣的接口(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
2
<script src="https://cdn.rawgit.com/Reactive-Extensions/RxJS/master/dist/rx.all.min.js"></script>
<script src="https://cdn.rawgit.com/Reactive-Extensions/RxJS-DOM/master/dist/rx.dom.min.js"></script>

只須要很簡單的一句工廠函數(factory method)就能夠將鼠標點擊的事件抽象成一個Observable。Rx.js提供一個全局對象RxRx.Observable就是Observable的類。

1
2
3
4
var loadButton = document.querySelector('.load');
var resultPanel = document.getElementById('result');

var click$ = Rx.Observable.fromEvent(loadButton, 'click');

click$就是前面的clickEventProcess,在這裏咱們將全部的Observable變量名結尾都添加$。點擊事件是像下面這樣子的:

1
[click ... click ........ click .. click ..... click ..........]

每一個點擊事件後應該發起一個網絡請求。

1
2
3
4
var response$$ = click$.map(function () {
// 爲了避免處理跨域問題,這裏換了個地址,返回和前面是同樣的
return Rx.DOM.get('http://output.jsbin.com/tafulo.json');
});

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
2
3
var response$ = click$.flatMap(function () {
return Rx.DOM.get('http://output.jsbin.com/tafulo.json');
});

圖示:

對於每個click事件,咱們都想將loading置爲true;而對於每次HTTP請求返回,則置爲false。因而,咱們能夠將click$映射成一個純粹的只含有true的Observable,但其每一個true到達的事件都和點擊事件到達的時間同樣;對於response$,一樣,將其映射呈只含有false的Observable。最後,咱們將兩個Observable結合在一塊兒(用Rx.Observable.merge),最終就能夠造成loading$,也就是剛纔咱們的loadingProcess

此外,$loading還應有一個初始值,能夠用startWith方法來指定。

1
2
3
4
var loading$ = Rx.Observable.merge(
click$.map(function () { return true; }),
response$.map(function () { return false; })
).startWith(false);

整個結合的過程如圖所示

有了loading$以後,咱們很快就能得出剛纔咱們所想要的textProcessenabledProcessenabledProcessloading$是一致的,就無需再生成,只要生成textProcess便可(命名爲text$)。

1
2
3
var text$ = loading$.map(function (loading) {
return loading ? 'Loading ...' : 'Load';
});

在Rx.js中沒有forEach方法,但有一個更好名字的方法,和forEach效用同樣,叫作subscribe。這樣咱們就能夠更新按鈕的樣式了。

1
2
3
4
5
6
7
8
9
10
11
text$.subscribe(function (text) {
$loadButton.text(text);
});
loading$.subscribe(function (loading) {
$loadButton.prop('disabled', loading);
});

// response$ 還能夠拿來更新#result的內容
response$.subscribe(function (data) {
$resultPanel.text('Got ' + JSON.parse(data.response).data.children.length + ' items');
});

這樣就用徹底Reactive的方式重構了以前咱們的例子。

在咱們重構後的方案中,消滅了全部的狀態。狀態都被Observable抽象了出去。因而,這樣的代碼若是放在一個函數裏面,這個函數將是沒有反作用的純函數。關於純函數、函數式編程,能夠閱讀個人文章《「函數是一等公民」背後的含義》

總結

本文從應用的角度入手解釋了Reactive Programming的思路。Observable做爲對狀態的抽象,統一了Iterative和Reactive,淡化了二者之間的邊界。固然,最大的好處就是咱們用抽象的形式將煩人的狀態趕出了視野,取而代之的是可組合的、可變換的Observable。

事物之間的對立統一一般很難找到。實際上,即便是在《設計模式》這本書中,做者們也不曾看到迭代器模式和觀察者模式之間存在的對稱關係。在UI設計領域,咱們更多地和用戶驅動、通訊驅動出來的事件打交道,這才促成了這兩個模式的合併。