原文地址:https://gist.github.com/staltz/868e7e9bc2a7b8c1f754
你所需要的一篇關(guān)于響應(yīng)式編程的介紹
前言
Rxjs 是一個實現(xiàn) Reactive Programming(響應(yīng)式編程) 思想的庫。
這篇文檔通篇說的都是 Reactive Programming,有時會簡稱為 Reactive,在這篇文檔中可以認(rèn)為 Reactive Programming 就是 Rxjs。
從更廣泛的意義來說,理解 Rxjs 背后的 Reactive Programming 的編程思想前弯,反過來對于學(xué)習(xí) Rxjs 有很大的幫助。
你應(yīng)該很好奇這個叫做Reactive Programming的新事物,特別是由它衍生的包括Rx磅网,Bacon.js,RAC 等的相關(guān)知識筷屡。
當(dāng)缺乏好的學(xué)習(xí)材料時涧偷,學(xué)習(xí) Reactive Programming 是很困難的。在我學(xué)習(xí)之初毙死,我只找到了少部分的實用的學(xué)習(xí)指南燎潮,而且它們只介紹表面的知識,沒有解決圍繞它來搭建整個架構(gòu)的挑戰(zhàn)扼倘。當(dāng)您試圖理解某個操作符時确封,官方文檔通常對你又沒有幫助。比如說:
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
將一個可觀測序列的每個元素通過合并元素的索引投射到一個新的可觀測序列序列中唉锌,然后將一個可觀測序列的可觀測序列轉(zhuǎn)換成一個僅從最近的可觀測序列產(chǎn)生值的可觀測序列隅肥。
天吶
我讀了兩本書竿奏,一本只描述了整體概況袄简,而另一本則是講如何使用 Reactive 庫。最終我艱難地學(xué)習(xí)了Reactive Programming:在用它構(gòu)建的同時搞清楚它泛啸。我在Futurice工作期間曾把它在一個真實的項目中绿语,在遇到困難時得到了一些同事的幫助。
學(xué)習(xí)過程中最困難的部分就是 thinking in Reactive(響應(yīng)式的思維方式)候址。這很大程度上是有關(guān)放棄傳統(tǒng)編程中舊有的命令式和有狀態(tài)的習(xí)慣的問題吕粹,并迫使你的大腦在不同的范式下工作。我在網(wǎng)上還沒有找到這方面的指南岗仑,我認(rèn)為這個世界應(yīng)該有一個關(guān)于 thinking in Reactive 的實用教程匹耕,這樣你就有一個指南了。官方文檔可以在此之后為你指明道路荠雕,我希望這對你有所幫助稳其。
什么是"Reactive Programming"?
網(wǎng)上有很多不好的解釋和定義炸卑。維基百科和往常一樣太籠統(tǒng)既鞠、太理論化了。 Stackoverflow的標(biāo)準(zhǔn)答案顯然不適合新手盖文。Reactive Manifesto(關(guān)于響應(yīng)式的宣言嘱蛋?)聽起來像是你向你的項目經(jīng)理或推銷時展示的那種東西。微軟的Rx術(shù)語“Rx = Observables + LINQ + Schedulers”是如此沉重和微軟,以至于我們大多數(shù)人都感到困惑洒敏。像“響應(yīng)式”和“變化的傳播”這樣的術(shù)語并沒有傳達(dá)出與你的典型MV *和你最喜歡的語言之間有什么不同龄恋。當(dāng)然,我的框架視圖會對模型做出反應(yīng)桐玻。當(dāng)然篙挽,變化是會傳播的。如果沒有镊靴,就不會呈現(xiàn)任何內(nèi)容铣卡。
那么,就讓我們廢話少說
Reactive programming是使用異步數(shù)據(jù)流進(jìn)行編程偏竟。
在某種程度上煮落,這并不是什么新鮮事。事件循環(huán)或典型的點擊事件實際上是一個異步事件流踊谋,您可以對其監(jiān)聽并執(zhí)行回調(diào)蝉仇。您可以創(chuàng)建任何內(nèi)容的數(shù)據(jù)流,而不僅僅是點擊和懸停事件殖蚕。流是廉價且無處不在的轿衔,任何東西都可以是流:變量,用戶輸入睦疫,屬性蛤育,緩存底洗,數(shù)據(jù)結(jié)構(gòu)等亥揖。例如,假設(shè)您的Twitter是一個數(shù)據(jù)流,其方式與點擊事件相同昼激。您可以監(jiān)聽該流并做出相應(yīng)的回應(yīng)瞧掺。
除此之外辟狈,Reactive programming還給你提供了一個功能集合哼转,用來組合壹蔓、創(chuàng)建和過濾這些流佣蓉。這就是“函數(shù)式編程”的魔力所在。一個流可以用作另一個流的輸入。甚至可以將多個流用作另一個流的輸入贞让。您可以合并兩個流续镇。您可以篩選一個流摸航,以獲得另一個流酱虎,該流只包含您感興趣的事件。您可以將數(shù)據(jù)值從一個流映射到另一個新的流擂涛。
既然流對于響應(yīng)式來說是如此重要,那么就讓我們仔細(xì)研究一下它們恢暖,從我們熟悉的“點擊按鈕”事件流開始排监。
流是按時間順序排列的一系列正在進(jìn)行的事件杰捂。它可以發(fā)出三種不同的事件:值(某種類型)舆床、錯誤或“完成”信號嫁佳。完成事件會發(fā)生在這種情況下蒿往,我們可能會做這樣的操作:關(guān)閉包含那個按鈕的窗口或者視圖組件。
我們只捕獲發(fā)出的這些異步事件俯在,通過定義一個函數(shù)執(zhí)行回調(diào)跷乐,該函數(shù)在值發(fā)出時執(zhí)行,在發(fā)出錯誤時執(zhí)行另一個函數(shù)趾浅,在發(fā)出“完成”時執(zhí)行另一個函數(shù)愕提。有時這最后兩個可以省略,您可以只專注于為值定義函數(shù)皿哨。對流的“監(jiān)聽”稱為訂閱浅侨。我們定義的函數(shù)是觀察者。流是被觀察的(或“可觀察的”)對象证膨。它就是是觀察者模式 Observer Design Pattern如输。
可以將上面的示意圖通過一種不同的方式來繪制數(shù)據(jù)流,這種方式叫彈珠圖央勒,我們將在本教程的某些部分使用彈珠圖不见,如:
--a---b-c---d---X---|->
a, b, c, d 表示發(fā)出的數(shù)據(jù)
X 表示錯誤
|表示 '結(jié)束' 信號
---> 是時間軸
概念方面已經(jīng)講了很多,為了不讓你感到無聊崔步,下面就讓我們來做一些操作:通過對原始點擊事件流進(jìn)行操作稳吮,生產(chǎn)一個新的點擊事件流。
首先井濒,我們創(chuàng)建一個記錄按鈕點擊次數(shù)的事件灶似。在常用的Reactive庫中慎陵,有許多操作符可以對流進(jìn)行處理,如map
喻奥、filter
席纽、scan
等。當(dāng)您調(diào)用其中一些操作符時撞蚕,例如 clickStream.map(f)
润梯,它會返回基于點擊事件流的一個新的事件流。它不會改變原始事件流甥厦。這叫做"數(shù)據(jù)不變性"(函數(shù)式的三個特性之一)的特性纺铭,它可以和響應(yīng)式時間流搭配在一起使用,就像豆?jié){和油條一樣完美的搭配刀疙。這使得我們可以通過鏈?zhǔn)秸{(diào)用的方式使用操作符舶赔,如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)
操作符會根據(jù)f
函數(shù)把原事件流中每一個返回值分別映射到新的事件流中。在上圖例子中谦秧,我們把每個點擊事件都映射成數(shù)字1竟纳。scan(g)
操作符把之前映射的值聚集起來,通過 x = g(accumulated, current)
(accumulated是累計值疚鲤,current是當(dāng)前值)的算法產(chǎn)生結(jié)果锥累,本例中g
就是一個累加函數(shù)。然后集歇,當(dāng)任一點擊發(fā)生時桶略,counterStream
都將發(fā)出累計點擊事件總數(shù)。
為了顯示 Reactive 真正的威力诲宇,我們假設(shè)您想要一個“雙擊”事件流际歼。為了讓它更有趣,我們假設(shè)這個事件流同時處理"三次點擊"或者"多次點擊"事件姑蓝。深呼吸鹅心,想象一下你將如何以一種傳統(tǒng)的命令式和有狀態(tài)的編程來實現(xiàn)。我敢打賭它掂,這實現(xiàn)起來相當(dāng)麻煩巴帮,涉及到需要定義一些變量來保存狀態(tài)溯泣,還得做一些時間間隔的調(diào)整虐秋。
通過 Reactive 處理就很簡單。實際上垃沦,邏輯部分只需要4行代碼客给。但是,當(dāng)前階段先讓我們忽略代碼部分肢簿。無論您是初學(xué)者還是專家靶剑,通過畫彈珠圖來幫助思考都是理解和構(gòu)建流的最佳方法蜻拨。
圖中,灰色方框表示將上面的事件流轉(zhuǎn)換為下面的事件流的過程函數(shù)桩引。首先我們根據(jù)點擊事件250ms的間隔時間組成列表(buffer(stream.throttle, 250ms)
做的事情)《兴希現(xiàn)在不著急取理解實現(xiàn)細(xì)節(jié),我們只關(guān)注演示 Reactive 的部分坑匠,buffer 的結(jié)果是生成一個由含有事件列表組成的流血崭。然后使用 map()
將每個列表映射成該列表的長度傳遞下去。最后厘灼,使用filter(x >= 2)
操作符來忽略小于1
的數(shù)字夹纫。就這樣通過3個操作符產(chǎn)生目標(biāo)的數(shù)據(jù)流。我們可以通過 subscribe (訂閱) 目標(biāo)的數(shù)據(jù)流來驗證結(jié)果是否符合我們的預(yù)期设凹。
"我為什么要采用PR(Reactive Programming的縮寫)舰讹?"
Reactive Programming 提高了代碼的抽象級別,因此你可以專注于定義業(yè)務(wù)邏輯的事件之間的相互依賴闪朱,而不必不斷地修改大量的實現(xiàn)細(xì)節(jié)月匣。RP中的代碼可能更簡潔。
這種優(yōu)勢在現(xiàn)代web應(yīng)用程序和移動應(yīng)用程序中更為明顯奋姿,這些應(yīng)用程序與大量與數(shù)據(jù)事件相關(guān)的頁面事件高度交互桶错。10年前,與web頁面的交互主要是向后端提交一個長表單胀蛮,并向前端執(zhí)行簡單的呈現(xiàn)院刁。應(yīng)用程序已經(jīng)進(jìn)化得更加實時:修改單個表單字段可以自動觸發(fā)保存到后端,對某些內(nèi)容的“贊”可以實時反映到其他連接的用戶粪狼,等等退腥。
如今的應(yīng)用程序擁有豐富的各種實時事件,為用戶提供了高度交互性的體驗再榄。我們需要合適的工具來處理這個問題狡刘,而Reactive Programming 就是一個答案。
通過例子來說明 Thinking in RP
讓我們來看看真正的東西困鸥。一個實際的例子嗅蔬,一步一步地指導(dǎo)如何在RP中思考。沒有綜合的例子疾就,沒有半解釋的概念澜术。在本教程結(jié)束時,我們將生成真正的功能代碼猬腰,同時知道為什么要這樣做鸟废。
我選擇 JavaScript 和 RxJS 作為工具是有原因的:JavaScript是目前最讓人熟悉的語言,而Rx* library系列應(yīng)用在許多語言和平臺中(NET姑荷、Java盒延、Scala缩擂、Clojure、JavaScript添寺、Ruby胯盯、Python、C++计露、Objective-C/Cocoa陨闹、Groovy等)。因此薄坏,無論您的工具是什么趋厉,您都可以通過學(xué)習(xí)本教程具體受益。
實現(xiàn)一個 "推薦關(guān)注" (Who to follow)的建議框
在Twitter中有一個頁面胶坠,向你推薦可以關(guān)注的其他賬戶:
我們將重點模仿它的3個主要功能:
- 開始階段君账,通過 API 加載推薦關(guān)注的用戶賬號數(shù)據(jù),顯示3個推薦用戶
- 點擊 "刷新按鈕"沈善,加載另外3個推薦用戶顯示到這三行中
- 單擊每行推薦用戶右上方的“x”按鈕乡数,只清除被點擊的用戶并顯示另一個用戶到當(dāng)前行
- 每一行都一個用戶的頭像,點擊可以鏈接到他們的主頁
我們可以先忽略其他功能和按鈕闻牡,因為它們是次要的净赴。Twitter最近對未經(jīng)授權(quán)的公眾關(guān)閉了它的 API,我們將用 Github API 獲取 users(https://api.github.com/users?since=135)代替Twitter構(gòu)建我們的頁面
請求與響應(yīng)
在Rx中如何用處理這個問題?首先罩润,(幾乎)所有東西都可以是一條流玖翅,這就是Rx的準(zhǔn)則。讓我們從最簡單的功能開始:“在開始階段割以,從API加載推薦關(guān)注的用戶賬戶數(shù)據(jù)金度,然后顯示三個推薦用戶”。這里沒有什么特殊严沥,就是 (1)發(fā)出一個請求猜极,(2)獲取響應(yīng)數(shù)據(jù),(3)渲染響應(yīng)數(shù)據(jù)消玄。我們把請求作為一個事件流跟伏。乍一看,這樣做似乎有點夸張翩瓜,但我們需要從基本的做起受扳,不是嗎?
開始時我們只需要做一個請求,如果我們將它作為一個數(shù)據(jù)流奥溺,它只能成為一個僅僅返回一個值的事件流而已辞色。一會兒我們還會有很多請求要做,但當(dāng)前浮定,只有一個相满。
--a------|->
a是一個字符串 'https://api.github.com/users'
這是我們要請求的url事件流。無論何時發(fā)生請求事件桦卒,它都會告訴我們兩件事:when 和 what立美。何時(when)發(fā)請求:當(dāng)事件發(fā)出的時候。請求什么(what):發(fā)出一個包含URL的字符串的值方灾。
var requestStream = Rx.Observable.just('https://api.github.com/users');
到現(xiàn)在建蹄,這只是一個字符串流,沒有其他操作裕偿,所以我們需要在這個值被釋放時做一些事情洞慎。這是通過subscribing(訂閱)流來實現(xiàn)的。
requestStream.subscribe(function(requestUrl) {
// 執(zhí)行請求
// onNext 改成 next,onError 改成 error, onComplete 改成 complete
var responseStream = Rx.Observable.create(function (observer) {
jQuery.getJSON(requestUrl)
.done(function(response) { observer.next(response); })
.fail(function(jqXHR, status, error) { observer.error(error); })
.always(function() { observer.complete(); });
});
responseStream.subscribe(function(response) {
// 接口響應(yīng)處理
});
}
注意到我們這里使用的是JQuery的AJAX回調(diào)方法來的處理這個異步的請求操作嘿棘。但是劲腿,請稍等一下,Rx就是用來處理異步數(shù)據(jù)流的鸟妙,難道它就不能處理來自請求(request)在未來某個時間響應(yīng)(response)的數(shù)據(jù)流嗎焦人?好吧,理論上是可以的重父,讓我們嘗試一下花椭。
Rx.Observable.create()
所做的是通過顯式地通知每個觀察者(或者說是“訂閱者”)關(guān)于數(shù)據(jù)事件(next()
或錯誤(error()
),來創(chuàng)建自定義流房午。我們只是簡單的封裝了一下 jQuery Ajax Promise 而已矿辽。這是否意味著 jQuery Ajax Promise 本質(zhì)上是一個 Observable(可觀察者)呢?
是的
Observable 是 Promise++(Promise的加強(qiáng)版)。在Rx中郭厌,通過 var stream = Rx.Observable. frompromise (Promise)
可以很容易地將一個Promise 轉(zhuǎn)換成一個Observable嗦锐。唯一的不同之處在于,Observable與 Promises/A+ 不兼容沪曙,但在理論上不沖突奕污。一個 Promise 就是一個只有一個返回值的 Observable。Rx流允許有多個值返回液走,更勝 Promise 一籌碳默。
這點很棒,說明 Observable比 Promise 更強(qiáng)大缘眶。如果您相信 Promise 宣傳的東西嘱根,可以留意一下 Rx Observable 能勝任什么。
回到我們的示例中巷懈,你應(yīng)該很快注意到该抒,我們在requestStream
的subscribe()
方法中又有一個subscribe()
調(diào)用,這有點類似于回調(diào)地獄顶燕。除此之外凑保,responseStream
的創(chuàng)建依賴于requestStream
冈爹。我們之前說過,在Rx中欧引,有很多簡單的機(jī)制從其他事件流轉(zhuǎn)換并創(chuàng)建出一個新的流频伤,所以我們也可以這樣做試試。
你現(xiàn)在需要了解的一個基本的操作符是 map(f)
,它可以從事件流A中取出每個值,對每個值執(zhí)行f()
函數(shù)芝此,然后將產(chǎn)生的新值填充到事件流B中憋肖。如果將它應(yīng)用到我們的請求和響應(yīng)事件流當(dāng)中,我們可以將請求 URLs 映射到響應(yīng) Promises上(偽裝成流)。
var responseMetastream = requestStream
.map(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
然后我們就創(chuàng)建了一個叫做“metastream”(高階流)的野獸:裝載了事件流的事件流婚苹。不要驚慌岸更。metastream也是一個流,其中每個發(fā)出的值又是另一個事件流膊升。您可以將它看作指針數(shù)組:每一個單獨發(fā)出的值就是一個指針怎炊,它指向另一個事件流。在我們的示例里用僧,每一個請求URL都映射到一個指向包含響應(yīng)數(shù)據(jù)的promise數(shù)據(jù)流结胀。
一個響應(yīng)的metastream的,看起來確實令人困惑责循,對我們似乎也沒什么幫助糟港。我們只想要一個簡單的響應(yīng)數(shù)據(jù)流,其每個發(fā)出的值都是JSON對象院仿,而不是一個“Promise”的JSON對象秸抚。讓我們來見識一下另一個函數(shù)Flatmap:map()
操作符的一個版本,它通過將在“分支”流上發(fā)出的所有內(nèi)容發(fā)到“主干”流來“打平”metastream歹垫。Flatmap不是“修復(fù)”metastream剥汤,metastream也不是一個bug,它們實際上是用于處理Rx中的異步響應(yīng)的好工具排惨。
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
由于響應(yīng)流是根據(jù)請求流定義的吭敢,如果以后有更多的事件發(fā)生在請求流上,我們就會有相應(yīng)的響應(yīng)事件發(fā)生在響應(yīng)流上暮芭,正如我們所期望的:
requestStream: --a-----b--c------------|->
responseStream: -----A--------B-----C---|->
(每個小寫字母都是一個請求, 大寫字母是對應(yīng)的響應(yīng))
現(xiàn)在我們終于有了一個響應(yīng)流鹿驼,并且可以用我們收到的數(shù)據(jù)來渲染了:
responseStream.subscribe(function(response) {
// 以你想要的方式將 response 響應(yīng)熏染到 DOM 中
});
讓我們把所有代碼合起來,看一下:
var requestStream = Rx.Observable.just('https://api.github.com/users');
var responseStream = requestStream
.flatMap(function(requestUrl) {
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
responseStream.subscribe(function(response) {
// 以你想要的方式將 response 響應(yīng)熏染到 DOM 中
});
刷新按鈕
我還沒有提到響應(yīng)中的 JSON 是一個包含100個用戶數(shù)據(jù)的列表辕宏。API只允許我們指定頁面offset畜晰,而不允許指定頁面大小宿百,因此我們只使用了3條數(shù)據(jù)對象魄揉,而浪費了97個其他對象。現(xiàn)在我們可以先忽略這個問題喂分,稍后我們將學(xué)習(xí)如何緩存響應(yīng)的數(shù)據(jù)。
每次單擊refresh按鈕時块蚌,請求流應(yīng)該發(fā)出一個新的URL闰非,以便我們能夠獲取新的響應(yīng)數(shù)據(jù)。我們需要兩件東西:刷新按鈕上的事件流(準(zhǔn)則:一切都可以作為流)匈子,我們需要將點擊刷新按鈕的事件流作為請求事件流的依賴(即點擊刷新事件流會引起請求事件流)河胎。幸運的是闯袒,RxJS 中有將事件監(jiān)聽器轉(zhuǎn)換成 Observables 的方法了虎敦。
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
由于刷新按鈕的點擊事件本身不攜帶任何API URL,所以我們需要將每次點擊映射到一個實際的URL≌遥現(xiàn)在其徙,我們將請求流更改為刷新按鈕的點擊事件流,該流每次都映射到API端點喷户,并帶有一個隨機(jī)偏移參數(shù)唾那。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
因為我比較笨而且也沒有使用自動化測試,所以我剛把之前做好的一個功能搞爛了褪尝。這樣闹获,請求在一開始的時候就不會執(zhí)行,而只有在點擊事件發(fā)生時才會執(zhí)行河哑。我們需要的是兩種情況都要執(zhí)行:剛開始打開網(wǎng)頁和點擊刷新按鈕都會執(zhí)行的請求避诽。
我們知道如何為每一種情況做一個單獨的事件流:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
但是我們怎樣才能把這兩者“合并”成一個呢?我們可以使用merge()
。在彈珠圖中解釋璃谨,它的功能如下:
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
現(xiàn)在應(yīng)該很容易:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
var requestStream = Rx.Observable.merge(
requestOnRefreshStream, startupRequestStream
);
有一種替代的更簡潔的寫法沙庐,不需要中間流。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.merge(Rx.Observable.just('https://api.github.com/users'));
有更簡短佳吞,更易讀的:
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.startWith('https://api.github.com/users');
startWith()
操作符如你預(yù)期的執(zhí)行了操作拱雏。無論您的輸入流看起來如何,startWith(x)
的輸出流在開始時都會有一個x
作為開頭底扳。但是我沒有總是DRY(Don't_repeat_yourself)铸抑,我在重復(fù)API的 URL字符串。改進(jìn)這個問題的方法是將startWith()
挪到refreshClickStream
那里衷模,以便在啟動時“模擬”一個刷新點擊事件鹊汛。
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var responseStream = refreshClickStream
.mergeMap((requestUrl)=>{
return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl));
});
好了。如果回到我“破壞自動化測試”的地方算芯,您應(yīng)該會看到對比兩個地方的唯一區(qū)別是我添加了startWith()
柒昏。
用事件流將3個推薦的用戶數(shù)據(jù)模型化
到目前為止,在responseStream的subscribe()發(fā)生的渲染步驟中熙揍,我們只稍微提及了一下推薦關(guān)注頁面≈暗唬現(xiàn)在有了刷新按鈕,我們會出現(xiàn)一個問題:當(dāng)你點擊刷新按鈕,當(dāng)前的三個推薦關(guān)注用戶沒有被清除有梆,而只要響應(yīng)的數(shù)據(jù)達(dá)到后我們就拿到了新的推薦關(guān)注的用戶數(shù)據(jù)是尖。為了讓UI看起來更漂亮,我們需要在點擊刷新按鈕的事件發(fā)生時清除當(dāng)前的三個推薦用戶泥耀。
refreshClickStream.subscribe(function() {
// 清除這三個推薦的 DOM 元素
});
不饺汹,老兄,還沒那么快痰催。我們又出現(xiàn)了新的問題兜辞,因為我們現(xiàn)在有兩個訂閱者影響著推薦用戶的 UI DOM元素(refreshClickStream.subscribe()
和responseStream.subscribe()
),這聽起來不符合 Separation of concerns(關(guān)注點分離)夸溶。還記得Reactive 的準(zhǔn)則嗎?
因此逸吵,讓我們把推薦關(guān)注的用戶數(shù)據(jù)模型化成事件流形式,每個被發(fā)出的值是一個包含推薦關(guān)注用戶數(shù)據(jù)的JSON對象缝裁。我們將對這3個用戶數(shù)據(jù)分開處理扫皱。下面是推薦關(guān)注的1號用戶數(shù)據(jù)的事件流:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// 從 listUsers 中獲取一個隨機(jī)用戶
return listUsers[Math.floor(Math.random()*listUsers.length)];
});
其他的,如推薦關(guān)注的2號用戶數(shù)據(jù)的事件流suggestion2Stream和推薦關(guān)注的3號用戶數(shù)據(jù)的事件流suggestion3Stream 都可以方便的從suggestion1Stream 復(fù)制粘貼就好捷绑。這里并不是重復(fù)代碼韩脑,只是為讓我們的示例更加簡單,而且我認(rèn)為這是一個思考如何避免重復(fù)代碼的好案例粹污。
我們不在responseStream的subscribe()中處理渲染了段多,我們在這里這樣做:
suggestion1Stream.subscribe(function(suggestion) {
// 將1號推薦用戶渲染到 DOM中
});
回到“當(dāng)刷新時,清除掉當(dāng)前的推薦關(guān)注的用戶”厕怜,我們可以簡單地將刷新按鈕點擊映射用戶推薦數(shù)據(jù)為空null
衩匣,并且在suggestion1Stream中包含進(jìn)來,如下所示:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// 從 listUsers 中獲取一個隨機(jī)的 user
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
);
在渲染時粥航,我們將null
解釋為“沒有數(shù)據(jù)”琅捏,然后把頁面元素隱藏起來。
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// 隱藏第一個推薦 DOM 元素
}
else {
// 顯示第一個推薦 DOM 元素递雀,渲染數(shù)據(jù)
}
});
現(xiàn)在我們的一個大的示意圖是這樣的:
refreshClickStream: ----------o--------o---->
requestStream: -r--------r--------r---->
responseStream: ----R---------R------R-->
suggestion1Stream: ----s-----N---s----N-s-->
suggestion2Stream: ----q-----N---q----N-q-->
suggestion3Stream: ----t-----N---t----N-t-->
N
代表null
柄延。
作為一種補充,我們可以在一開始的時候就渲染空的推薦內(nèi)容缀程。這通過把startWith(null)添加到推薦關(guān)注的事件流就可以了:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// 從 listUsers 中獲取一個隨機(jī)的 user
return listUsers[Math.floor(Math.random()*listUsers.length)];
})
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
結(jié)果為:
refreshClickStream: ----------o---------o---->
requestStream: -r--------r---------r---->
responseStream: ----R----------R------R-->
suggestion1Stream: -N--s-----N----s----N-s-->
suggestion2Stream: -N--q-----N----q----N-q-->
suggestion3Stream: -N--t-----N----t----N-t-->
推薦關(guān)注的關(guān)閉和使用已緩存的響應(yīng)數(shù)據(jù)
只剩這一個功能沒有實現(xiàn)了搜吧,每個推薦關(guān)注的用戶UI會有一個'x'按鈕來關(guān)閉自己,然后在當(dāng)前的用戶數(shù)據(jù)UI中加載另一個推薦關(guān)注的用戶杨凑。最初的想法是:點擊任何關(guān)閉按鈕時都需要發(fā)起一個新的請求:
var close1Button = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
// 第二三個推薦用戶的關(guān)閉按鈕也是一樣的操作
var requestStream = refreshClickStream.startWith('startup click')
.merge(close1ClickStream) // 加上這行
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
這樣沒什么效果滤奈,這樣會關(guān)閉和重新加載全部的推薦關(guān)注用戶,而不僅僅是處理我們點擊的那一個撩满。這里有幾種方式來解決這個問題蜒程,并且讓它變得有趣绅你,我們將重用之前的請求數(shù)據(jù)來解決這個問題。這個API響應(yīng)的每頁數(shù)據(jù)大小是100個用戶數(shù)據(jù)昭躺,而我們只使用了其中三個忌锯,所以還有一大堆未使用的數(shù)據(jù)可以拿來用,不用去請求更多數(shù)據(jù)了领炫。
接下來偶垮,我們繼續(xù)用事件流的方式來思考。當(dāng)'close1'點擊事件發(fā)生時帝洪,我們想要使用最近發(fā)出的響應(yīng)數(shù)據(jù)似舵,并執(zhí)行responseStream函數(shù)來從響應(yīng)列表里隨機(jī)的抽出一個用戶數(shù)據(jù)來,就像下面這樣:
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
在 Rx* 中有一個組合操作符叫 combineLatest
碟狞,它似乎能夠?qū)崿F(xiàn)我們的需求啄枕。它接受兩個數(shù)據(jù)流A和B作為輸入婚陪,并且無論哪一個數(shù)據(jù)流發(fā)出一個值了族沃,combineLatest
就將從兩個數(shù)據(jù)流最近發(fā)出的值a和b作為f函數(shù)的輸入,計算后返回一個輸出值(c = f(x,y)
)泌参。用彈珠圖會讓這個函數(shù)的過程看起來比較好理解:
stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
vvvvvvvv combineLatest(f) vvvvvvv
----AB---AC--EC---ED--ID--IQ---->
f 是將字符轉(zhuǎn)大寫字母的函數(shù)
這樣我們可以在close1ClickStream
和responseStream
上應(yīng)用combineLatest()脆淹,只要點擊close1按鈕,我們就可以獲得最近的響應(yīng)數(shù)據(jù)沽一,并在suggestion1Stream
上產(chǎn)生一個新值盖溺。另一方面,combineLatest()也是相對的:每當(dāng)在responseStream
上發(fā)出一個新的響應(yīng)時铣缠,它將會結(jié)合一個新的點擊關(guān)閉按鈕事件來產(chǎn)生一個新的推薦關(guān)注的用戶數(shù)據(jù)烘嘱。這很有趣,因為它可以給我們簡化suggestion1Stream
的代碼蝗蛙,例如:
var suggestion1Stream = close1ClickStream
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
現(xiàn)在蝇庭,我們的代碼拼圖中還缺一塊。combineLatest()使用兩個最近的數(shù)據(jù)源捡硅,但是如果其中一個源還沒有發(fā)出任何東西哮内,combineLatest()就不能在輸出流上生成數(shù)據(jù)事件。如果您查看上面的彈珠圖壮韭,您會看到當(dāng)?shù)谝粋€流發(fā)出值a時北发,輸出為空。只有當(dāng)?shù)诙€流發(fā)出值b時喷屋,才會產(chǎn)生輸出值琳拨。
這里有很多種方法來解決這個問題,我們使用最簡單的一種屯曹,也就是在啟動的時候模擬'close 1'的點擊事件:
var suggestion1Stream = close1ClickStream.startWith('startup click') // 添加這行代碼
.combineLatest(responseStream,
function(click, listUsers) {l
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
封裝
我們已經(jīng)完成了狱庇。下面是封裝好的完整示例代碼:
var refreshButton = document.querySelector('.refresh');
var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
var closeButton1 = document.querySelector('.close1');
var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');
// close2 和 close3 按鈕也是一樣的邏輯
var requestStream = refreshClickStream.startWith('startup click')
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var responseStream = requestStream
.flatMap(function (requestUrl) {
return Rx.Observable.fromPromise($.ajax({url: requestUrl}));
});
var suggestion1Stream = close1ClickStream.startWith('startup click')
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);
// suggestion2Stream 和 suggestion3Stream 推薦流也是一樣的處理邏輯
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// 隱藏第一個推薦 DOM 元素
}
else {
// 顯示第一個推薦 DOM 元素寄疏,渲染數(shù)據(jù)
});
您可以看到可演示的示例工程:http://jsfiddle.net/staltz/8jFJH/48/
以上的代碼片段雖小但做到很多事:它適當(dāng)?shù)厥褂藐P(guān)注分離原則(separation of concerns)實現(xiàn)了對多個事件流的管理,甚至做到了響應(yīng)數(shù)據(jù)的緩存僵井。這種函數(shù)式的風(fēng)格使得代碼看起來更像是聲明式編程而非命令式編程陕截,我們并不是在給一組指令去執(zhí)行,只是定義了事件流之間關(guān)系來告訴它這是什么批什。例如农曲,在Rx中我們告訴計算機(jī)suggtion1stream
是suggtion1stream
是'close 1'事件結(jié)合從最新的響應(yīng)數(shù)據(jù)中拿到的一個用戶數(shù)據(jù)的數(shù)據(jù)流,除此之外驻债,當(dāng)刷新事件發(fā)生時和程序啟動時乳规,它就是null。
留意一下代碼中并未出現(xiàn)例如if
合呐、for
暮的、while
等流程控制語句,或者像JavaScript程序中典型的基于回調(diào)的流程控制淌实。如果可以的話冻辩,你甚至可以在subscribe()上使用filter()
來開拓if
和else
(我將把實現(xiàn)細(xì)節(jié)留給您作為練習(xí))。在Rx中拆祈,我們有諸如map
恨闪、filter
、scan
放坏、merge
咙咽、combineLatest
、startWith
等數(shù)據(jù)流操作符淤年。還有很多函數(shù)可以用來控制事件驅(qū)動編程的流程钧敞。這些函數(shù)的集合可以讓你使用更少的代碼實現(xiàn)更強(qiáng)大的功能。
接下來
如果您認(rèn)為 Rx* 將是您首選的 Reactive Programming 庫麸粮,那么請花些時間來熟悉big list of functions(操作符列表)用來轉(zhuǎn)換溉苛、組合和創(chuàng)建 Observables。如果您想通過彈珠圖了解這些操作符豹休,請查看 RxJava's very useful documentation with marble diagrams炊昆。無論何時你遇到問題,畫出那些彈珠圖威根,思考一下凤巨,看看一大串操作符,然后繼續(xù)思考洛搀。根據(jù)我的經(jīng)驗敢茁,這樣效果很有效。
一旦您開始使用 Rx* 編程的竅門留美,就必須要理解Cold 與 Hot Observables的概念彰檬。如果你忽視這一點伸刃,它會回過頭狠狠地咬你一口。我這里已經(jīng)警告你了逢倍,學(xué)習(xí)函數(shù)式編程捧颅,并熟悉影響Rx*的副作用等問題,將進(jìn)一步提升您的技能较雕。
但是 Reactive Programming 不僅僅是 Rx碉哑。像Bacon.js,它使用起來很直觀亮蒋,沒有您在 Rx 中有時會遇到的怪癖扣典。Elm 語言則以它自己的方式支持響應(yīng)式編程:它是一種可以編譯為JavaScript + HTML + CSS的 Reactive Programming,并且有一個time travelling debugger功能慎玖,非常棒贮尖。
Rx 適用于事件較多的前端和應(yīng)用程序。但這不僅僅是客戶端問題趁怔,還可以用在后端或者接近數(shù)據(jù)庫的地方湿硝。事實上,RxJava是Netflix 服務(wù)端 API 用來處理并行的的組件痕钢。Rx不局限于一種特定類型的應(yīng)用程序或語言图柏。它真的是你編寫任何事件驅(qū)動程序、可以遵循的一個非常棒的編程范式任连。
如果本教程對您有幫助,請轉(zhuǎn)發(fā)它例诀。