相信你們?cè)趯W(xué)習(xí)響應(yīng)式編程這個(gè)新技術(shù)的時(shí)候都會(huì)充滿了好奇,特別是它的一些變體,例如:Rx系列、Bacon.js、RAC等等……
在缺乏優(yōu)秀資料的前提下,響應(yīng)式編程的學(xué)習(xí)過(guò)程將滿是荊棘。起初,我試圖尋找一些教程,卻只找到少量的實(shí)踐指南,而且它們講的都非常淺顯,從來(lái)沒(méi)人接受圍繞響應(yīng)式編程建立一個(gè)完整知識(shí)體系的挑戰(zhàn)。此外,官方文檔通常也不能很好地幫助你理解某些函數(shù),因?yàn)樗鼈兺ǔ?雌饋?lái)很繞,不信請(qǐng)看這里:
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
根據(jù)元素下標(biāo),將可觀察序列中每個(gè)元素一一映射到一個(gè)新的可觀察序列當(dāng)中,然后...%…………%&¥#@@……&**(暈了)
天吶,這簡(jiǎn)直太繞了!
我讀過(guò)兩本相關(guān)的書(shū),一本只是在給你描繪響應(yīng)式編程的偉大景象,而另一本卻只是深入到如何使用響應(yīng)式庫(kù)而已。我在不斷的構(gòu)建項(xiàng)目過(guò)程中把響應(yīng)式編程了解的透徹了一些,最后以這種艱難的方式學(xué)完了響應(yīng)式編程。在我工作公司的一個(gè)實(shí)際項(xiàng)目中我會(huì)用到它,當(dāng)我遇到問(wèn)題時(shí),還可以得到同事的支持。
學(xué)習(xí)過(guò)程中最難的部分是如何以響應(yīng)式的方式來(lái)思考,更多的意味著要摒棄那些老舊的命令式和狀態(tài)式的典型編程習(xí)慣,并且強(qiáng)迫自己的大腦以不同的范式來(lái)運(yùn)作。我還沒(méi)有在網(wǎng)絡(luò)上找到任何一個(gè)教程是從這個(gè)層面來(lái)剖析的,我覺(jué)得這個(gè)世界非常值得擁有一個(gè)優(yōu)秀的實(shí)踐教程來(lái)教你如何以響應(yīng)式編程的方式來(lái)思考,方便引導(dǎo)你開(kāi)始學(xué)習(xí)響應(yīng)式編程。然后看各種庫(kù)文檔才可以給你更多的指引。希望這篇文章能夠幫助你快速地進(jìn)入響應(yīng)式編程的世界。
網(wǎng)絡(luò)上有一大堆糟糕的解釋和定義,如Wikipedia上通常都是些非常籠統(tǒng)和理論性的解釋,而Stackoverflow上的一些規(guī)范的回答顯然也不適合新手來(lái)參考,Reactive Manifesto看起來(lái)也只像是拿給你的PM或者老板看的東西,微軟的 所以,不要再扯這些廢話了。 一方面,這已經(jīng)不是什么新事物了。事件總線(Event Buses)或一些典型的點(diǎn)擊事件本質(zhì)上就是一個(gè)異步事件流(asynchronous event stream),這樣你就可以觀察它的變化并使其做出一些反應(yīng)(do some side effects)。響應(yīng)式是這樣的一個(gè)思路:除了點(diǎn)擊和懸停(hover)的事件,你還可以給其他任何事物創(chuàng)建數(shù)據(jù)流。數(shù)據(jù)流無(wú)處不在,任何東西都可以成為一個(gè)數(shù)據(jù)流,例如變量、用戶輸入、屬性、緩存、數(shù)據(jù)結(jié)構(gòu)等等。舉個(gè)栗子,你可以把你的微博訂閱功能想象成跟點(diǎn)擊事件一樣的數(shù)據(jù)流,你可以監(jiān)聽(tīng)這樣的數(shù)據(jù)流,并做出相應(yīng)的反應(yīng)。 最重要的是,你會(huì)擁有一些令人驚艷的函數(shù)去結(jié)合、創(chuàng)建和過(guò)濾任何一組數(shù)據(jù)流。 這就是"函數(shù)式編程"的魔力所在。一個(gè)數(shù)據(jù)流可以作為另一個(gè)數(shù)據(jù)流的輸入,甚至多個(gè)數(shù)據(jù)流也可以作為另一個(gè)數(shù)據(jù)流的輸入。你可以合并兩個(gè)數(shù)據(jù)流,也可以過(guò)濾一個(gè)數(shù)據(jù)流得到另一個(gè)只包含你感興趣的事件的數(shù)據(jù)流,還可以映射一個(gè)數(shù)據(jù)流的值到一個(gè)新的數(shù)據(jù)流里。 數(shù)據(jù)流是整個(gè)響應(yīng)式編程體系中的核心,要想學(xué)習(xí)響應(yīng)式編程,當(dāng)然要先走進(jìn)數(shù)據(jù)流一探究竟了。那現(xiàn)在就讓我們先從熟悉的"點(diǎn)擊一個(gè)按鈕"的事件流開(kāi)始 一個(gè)數(shù)據(jù)流是一個(gè)按時(shí)間排序的即將發(fā)生的事件(Ongoing events ordered in time)的序列。如上圖,它可以發(fā)出3種不同的事件(上一句已經(jīng)把它們叫做事件):一個(gè)某種類型的值事件,一個(gè)錯(cuò)誤事件和一個(gè)完成事件。當(dāng)一個(gè)完成事件發(fā)生時(shí),在某些情況下,我們可能會(huì)做這樣的操作:關(guān)閉包含那個(gè)按鈕的窗口或者視圖組件。 我們只能異步捕捉被發(fā)出的事件,使得我們可以在發(fā)出一個(gè)值事件時(shí)執(zhí)行一個(gè)函數(shù),發(fā)出錯(cuò)誤事件時(shí)執(zhí)行一個(gè)函數(shù),發(fā)出完成事件時(shí)執(zhí)行另一個(gè)函數(shù)。有時(shí)候你可以忽略后兩個(gè)事件,只需聚焦于如何定義和設(shè)計(jì)在發(fā)出值事件時(shí)要執(zhí)行的函數(shù),監(jiān)聽(tīng)這個(gè)事件流的過(guò)程叫做訂閱,我們定義的函數(shù)叫做觀察者,而事件流就可以叫做被觀察的主題(或者叫被觀察者)。你應(yīng)該察覺(jué)到了,對(duì)的,它就是觀察者模式。 上面的示意圖我們也可以用ASCII碼的形式重新畫(huà)一遍,請(qǐng)注意,下面的部分教程中我們會(huì)繼續(xù)使用這幅圖: 現(xiàn)在你對(duì)響應(yīng)式編程事件流應(yīng)該非常熟悉了,為了不讓你感到無(wú)聊,讓我們來(lái)做一些新的嘗試吧:我們將創(chuàng)建一個(gè)由原始點(diǎn)擊事件流演變而來(lái)的一種新的點(diǎn)擊事件流。 首先,讓我們來(lái)創(chuàng)建一個(gè)記錄按鈕點(diǎn)擊次數(shù)的事件流。在常用的響應(yīng)式庫(kù)中,每個(gè)事件流都會(huì)附有一些函數(shù),例如 為了展示響應(yīng)式編程真正的魅力,我們假設(shè)你有一個(gè)"雙擊"事件流,為了讓它更有趣,我們假設(shè)這個(gè)事件流同時(shí)處理"三次點(diǎn)擊"或者"多次點(diǎn)擊"事件,然后深吸一口氣想想如何用傳統(tǒng)的命令式和狀態(tài)式的方式來(lái)處理,我敢打賭,這么做會(huì)相當(dāng)?shù)挠憛?,其中還要涉及到一些變量來(lái)保存狀態(tài),并且還得做一些時(shí)間間隔的調(diào)整。 而用響應(yīng)式編程的方式處理會(huì)非常的簡(jiǎn)潔,實(shí)際上,邏輯處理部分只需要四行代碼。但是,當(dāng)前階段讓我們現(xiàn)忽略代碼的部分,無(wú)論你是新手還是專家,看著圖表思考來(lái)理解和建立事件流將是一個(gè)非常棒的方法。 圖中,灰色盒子表示將上面的事件流轉(zhuǎn)換下面的事件流的函數(shù)過(guò)程,首先根據(jù)250毫秒的間隔時(shí)間(event silence, 譯者注:無(wú)事件發(fā)生的時(shí)間段,上一個(gè)事件發(fā)生到下一個(gè)事件發(fā)生的間隔時(shí)間)把點(diǎn)擊事件流一段一隔開(kāi),再將每一段的一個(gè)或多個(gè)點(diǎn)擊事件添加到列表中(這就是這個(gè)函數(shù): 我希望你能感受到這個(gè)示例的優(yōu)雅之處。當(dāng)然了,這個(gè)示例也只是響應(yīng)式編程魔力的冰山一角而已,你同樣可以將這3步操作應(yīng)用到不同種類的事件流中去,例如,一串API響應(yīng)的事件流。另一方面,你還有非常多的函數(shù)可以使用。 響應(yīng)式編程可以加深你代碼抽象的程度,讓你可以更專注于定義與事件相互依賴的業(yè)務(wù)邏輯,而不是把大量精力放在實(shí)現(xiàn)細(xì)節(jié)上,同時(shí),使用響應(yīng)式編程還能讓你的代碼變得更加簡(jiǎn)潔。 特別對(duì)于現(xiàn)在流行的webapps和mobile apps,它們的 UI 事件與數(shù)據(jù)頻繁地產(chǎn)生交互,在開(kāi)發(fā)這些應(yīng)用時(shí)使用響應(yīng)式編程的優(yōu)點(diǎn)將更加明顯。十年前,web頁(yè)面的交互是通過(guò)提交一個(gè)很長(zhǎng)的表單數(shù)據(jù)到后端,然后再做一些簡(jiǎn)單的前端渲染操作。而現(xiàn)在的Apps則演變的更具有實(shí)時(shí)性:僅僅修改一個(gè)單獨(dú)的表單域就能自動(dòng)的觸發(fā)保存到后端的代碼,就像某個(gè)用戶對(duì)一些內(nèi)容點(diǎn)了贊,就能夠?qū)崟r(shí)反映到其他已連接的用戶一樣,等等。 當(dāng)今的Apps都含有豐富的實(shí)時(shí)事件來(lái)保證一個(gè)高效的用戶體驗(yàn),我們就需要采用一個(gè)合適的工具來(lái)處理,那么響應(yīng)式編程就正好是我們想要的答案。 讓我們深入到一些真實(shí)的例子,一個(gè)能夠一步一步教你如何以響應(yīng)式編程的方式思考的例子,沒(méi)有虛構(gòu)的示例,沒(méi)有一知半解的概念。在這個(gè)教程的末尾我們將產(chǎn)生一些真實(shí)的函數(shù)代碼,并能夠知曉每一步為什么那樣做的原因(知其然,知其所以然)。 我選了JavaScript和RxJS來(lái)作為本教程的編程語(yǔ)言,原因是:JavaScript是目前最多人熟悉的語(yǔ)言,而Rx系列的庫(kù)對(duì)于很多語(yǔ)言和平臺(tái)的運(yùn)用是非常廣泛的,例如(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa,Groovy等等。所以,無(wú)論你用的是什么語(yǔ)言、庫(kù)、工具,你都能從下面這個(gè)教程中學(xué)到東西(從中受益)。 在Twitter里有一個(gè)UI元素向你推薦你可以關(guān)注的用戶,如下圖: 我們將聚焦于模仿它的主要功能,它們是: 我們可以先不管其他的功能和按鈕,因?yàn)樗鼈兪谴我?。因?yàn)門witter最近關(guān)閉了未經(jīng)授權(quán)的公共API調(diào)用,我們將用Github獲取用戶的API代替,并且以此來(lái)構(gòu)建我們的UI。 如果你想先看一下最終效果,這里有完成后的 開(kāi)始時(shí)我們只需做一次請(qǐng)求,如果我們把它作為一個(gè)數(shù)據(jù)流的話,它只能成為一個(gè)僅僅返回一個(gè)值的事件流而已。一會(huì)兒我們還會(huì)有很多請(qǐng)求要做,但當(dāng)前,只有一個(gè)。 這是一個(gè)我們要請(qǐng)求的URL事件流。每當(dāng)發(fā)生一個(gè)請(qǐng)求時(shí),它將告訴我們兩件事:什么時(shí)候和做了什么事(when and what)。什么時(shí)候請(qǐng)求被執(zhí)行,什么時(shí)候事件就被發(fā)出。而做了什么就是請(qǐng)求了什么,也就是請(qǐng)求的URL字符串。 在Rx中,創(chuàng)建返回一個(gè)值的事件流是非常簡(jiǎn)單的。其實(shí)事件流在Rx里的術(shù)語(yǔ)是叫"被觀察者",也就是說(shuō)它是可以被觀察的,但是我發(fā)現(xiàn)這名字比較傻,所以我更喜歡把它叫做事件流。 但現(xiàn)在,這只是一個(gè)字符串的事件流而已,并沒(méi)有做其他操作,所以我們需要在發(fā)出這個(gè)值的時(shí)候做一些我們要做的操作,可以通過(guò)訂閱(subscribing)這個(gè)事件來(lái)實(shí)現(xiàn)。 注意到我們這里使用的是JQuery的AJAX回調(diào)方法(我們假設(shè)你已經(jīng)很了解JQuery和AJAX了)來(lái)的處理這個(gè)異步的請(qǐng)求操作。但是,請(qǐng)稍等一下,Rx就是用來(lái)處理異步數(shù)據(jù)流的,難道它就不能處理來(lái)自請(qǐng)求(request)在未來(lái)某個(gè)時(shí)間響應(yīng)(response)的數(shù)據(jù)流嗎?好吧,理論上是可以的,讓我們嘗試一下。 是的。 Promise++就是被觀察者(Observable),在Rx里你可以使用這樣的操作: 這樣更好,這樣更突出被觀察者至少比Promise強(qiáng)大,所以如果你相信Promise宣傳的東西,那么也請(qǐng)留意一下響應(yīng)式編程能勝任些什么。 現(xiàn)在回到示例當(dāng)中,你應(yīng)該能快速發(fā)現(xiàn),我們?cè)?code>subscribe()方法的內(nèi)部再次調(diào)用了 現(xiàn)在你需要了解的一個(gè)最基本的函數(shù)是 然后,我們創(chuàng)造了一個(gè)叫做"metastream"的怪獸:一個(gè)裝載了事件流的事件流。先別驚慌,metastream就是每一個(gè)發(fā)出的值都是另一個(gè)事件流的事件流,你看把它想象成一個(gè)[指針(pointers)]((https://en.wikipedia.org/wiki/Pointer_(computer_programming))數(shù)組:每一個(gè)單獨(dú)發(fā)出的值就是一個(gè)_指針_,它指向另一個(gè)事件流。在我們的示例里,每一個(gè)請(qǐng)求URL都映射到一個(gè)指向包含響應(yīng)數(shù)據(jù)的promise數(shù)據(jù)流。 一個(gè)響應(yīng)的metastream,看起來(lái)確實(shí)讓人容易困惑,看樣子對(duì)我們一點(diǎn)幫助也沒(méi)有。我們只想要一個(gè)簡(jiǎn)單的響應(yīng)數(shù)據(jù)流,每一個(gè)發(fā)出的值是一個(gè)簡(jiǎn)單的JSON對(duì)象就行,而不是一個(gè)'Promise' 的JSON對(duì)象。ok,讓我們來(lái)見(jiàn)識(shí)一下另一個(gè)函數(shù): 很贊,因?yàn)槲覀兊捻憫?yīng)事件流是根據(jù)請(qǐng)求事件流定義的,如果我們以后有更多事件發(fā)生在請(qǐng)求事件流的話,我們也將會(huì)在相應(yīng)的響應(yīng)事件流收到響應(yīng)事件,就如所期待的那樣: 現(xiàn)在,我們終于有響應(yīng)的事件流了,并且可以用我們收到的數(shù)據(jù)來(lái)渲染了: 讓我們把所有代碼合起來(lái),看一下: 我還沒(méi)提到本次響應(yīng)的JSON數(shù)據(jù)是含有100個(gè)用戶數(shù)據(jù)的list,這個(gè)API只允許指定頁(yè)面偏移量(page offset),而不能指定每頁(yè)大小(page size),我們只用到了3個(gè)用戶數(shù)據(jù)而浪費(fèi)了其他97個(gè),現(xiàn)在可以先忽略這個(gè)問(wèn)題,稍后我們將學(xué)習(xí)如何緩存響應(yīng)的數(shù)據(jù)。 每當(dāng)刷新按鈕被點(diǎn)擊,請(qǐng)求事件流就會(huì)發(fā)出一個(gè)新的URL值,這樣我們就可以獲取新的響應(yīng)數(shù)據(jù)。這里我們需要兩個(gè)東西:點(diǎn)擊刷新按鈕的事件流(準(zhǔn)則:一切都能作為事件流),我們需要將點(diǎn)擊刷新按鈕的事件流作為請(qǐng)求事件流的依賴(即點(diǎn)擊刷新事件流會(huì)引起請(qǐng)求事件流)。幸運(yùn)的是,RxJS已經(jīng)有了可以從事件監(jiān)聽(tīng)者轉(zhuǎn)換成被觀察者的方法了。 因?yàn)樗⑿掳粹o點(diǎn)擊事件不會(huì)攜帶將要請(qǐng)求的API的URL,我們需要將每次的點(diǎn)擊映射到一個(gè)實(shí)際的URL上,現(xiàn)在我們將請(qǐng)求事件流轉(zhuǎn)換成了一個(gè)點(diǎn)擊事件流,并將每次的點(diǎn)擊映射成一個(gè)隨機(jī)的頁(yè)面偏移量(offset)參數(shù)來(lái)組成API的URL。 因?yàn)槲冶容^笨而且也沒(méi)有使用自動(dòng)化測(cè)試,所以我剛把之前做好的一個(gè)功能搞爛了。這樣,請(qǐng)求在一開(kāi)始的時(shí)候就不會(huì)執(zhí)行,而只有在點(diǎn)擊事件發(fā)生時(shí)才會(huì)執(zhí)行。我們需要的是兩種情況都要執(zhí)行:剛開(kāi)始打開(kāi)網(wǎng)頁(yè)和點(diǎn)擊刷新按鈕都會(huì)執(zhí)行的請(qǐng)求。 我們知道如何為每一種情況做一個(gè)單獨(dú)的事件流: 但是我們是否可以將這兩個(gè)合并成一個(gè)呢?沒(méi)錯(cuò),是可以的,我們可以使用 現(xiàn)在做起來(lái)應(yīng)該很簡(jiǎn)單: 還有一個(gè)更干凈的寫(xiě)法,省去了中間事件流變量: 甚至可以更簡(jiǎn)短,更具有可讀性: 不錯(cuò),如果你倒回到"搞爛了的自動(dòng)測(cè)試"的地方,然后再對(duì)比這兩個(gè)地方,你會(huì)發(fā)現(xiàn)我僅僅是加了一個(gè) 直到現(xiàn)在,在響應(yīng)事件流(responseStream)的訂閱( 不,老兄,還沒(méi)那么快。我們又出現(xiàn)了新的問(wèn)題,因?yàn)槲覀儸F(xiàn)在有兩個(gè)訂閱者在影響著推薦關(guān)注的UI DOM元素(另一個(gè)是 現(xiàn)在,讓我們把推薦關(guān)注的用戶數(shù)據(jù)模型化成事件流形式,每個(gè)被發(fā)出的值是一個(gè)包含了推薦關(guān)注用戶數(shù)據(jù)的JSON對(duì)象。我們將把這三個(gè)用戶數(shù)據(jù)分開(kāi)處理,下面是推薦關(guān)注的1號(hào)用戶數(shù)據(jù)的事件流: 其他的,如推薦關(guān)注的2號(hào)用戶數(shù)據(jù)的事件流 Instead of having the rendering happen in responseStream's subscribe(), we do that here: 我們不在responseStream的subscribe()中處理渲染了,我們這樣處理: 回到"當(dāng)刷新時(shí),清楚掉當(dāng)前的推薦關(guān)注的用戶",我們可以很簡(jiǎn)單的把刷新點(diǎn)擊映射為沒(méi)有推薦數(shù)據(jù)( 當(dāng)渲染時(shí),我們將 現(xiàn)在我們大概的示意圖如下: 作為一種補(bǔ)充,我們可以在一開(kāi)始的時(shí)候就渲染空的推薦內(nèi)容。這通過(guò)把startWith(null)添加到推薦關(guān)注的事件流就可以了: 結(jié)果是這樣的: 只剩這一個(gè)功能沒(méi)有實(shí)現(xiàn)了,每個(gè)推薦關(guān)注的用戶UI會(huì)有一個(gè)'x'按鈕來(lái)關(guān)閉自己,然后在當(dāng)前的用戶數(shù)據(jù)UI中加載另一個(gè)推薦關(guān)注的用戶。最初的想法是:點(diǎn)擊任何關(guān)閉按鈕時(shí)都需要發(fā)起一個(gè)新的請(qǐng)求: 這樣沒(méi)什么效果,這樣會(huì)關(guān)閉和重新加載全部的推薦關(guān)注用戶,而不僅僅是處理我們點(diǎn)擊的那一個(gè)。這里有幾種方式來(lái)解決這個(gè)問(wèn)題,并且讓它變得有趣,我們將重用之前的請(qǐng)求數(shù)據(jù)來(lái)解決這個(gè)問(wèn)題。這個(gè)API響應(yīng)的每頁(yè)數(shù)據(jù)大小是100個(gè)用戶數(shù)據(jù),而我們只使用了其中三個(gè),所以還有一大堆未使用的數(shù)據(jù)可以拿來(lái)用,不用去請(qǐng)求更多數(shù)據(jù)了。 ok,再來(lái),我們繼續(xù)用事件流的方式來(lái)思考。當(dāng)'close1'點(diǎn)擊事件發(fā)生時(shí),我們想要使用最近發(fā)出的響應(yīng)數(shù)據(jù),并執(zhí)行 在Rx中一個(gè)組合函數(shù)叫做 這樣,我們就可以把 現(xiàn)在,我們的拼圖還缺一小塊地方。 這里有很多種方法來(lái)解決這個(gè)問(wèn)題,我們使用最簡(jiǎn)單的一種,也就是在啟動(dòng)的時(shí)候模擬'close 1'的點(diǎn)擊事件: 我們完成了,下面是封裝好的完整示例代碼: 你可以在這里看到可演示的示例工程 以上的代碼片段雖小但做到很多事:它適當(dāng)?shù)氖褂藐P(guān)注分離(separation of concerns)原則的實(shí)現(xiàn)了對(duì)多個(gè)事件流的管理,甚至做到了響應(yīng)數(shù)據(jù)的緩存。這種函數(shù)式的風(fēng)格使得代碼看起來(lái)更像是聲明式編程而非命令式編程:我們并不是在給一組指令去執(zhí)行,只是定義了事件流之間關(guān)系來(lái)告訴它這是什么。例如,我們用Rx來(lái)告訴計(jì)算機(jī) 留意一下代碼中并未出現(xiàn)例如 如果你認(rèn)為Rx將會(huì)成為你首選的響應(yīng)式編程庫(kù),接下來(lái)就需要花一些時(shí)間來(lái)熟悉一大批的函數(shù)用來(lái)變形、聯(lián)合和創(chuàng)建被觀察者。如果你想在事件流的圖表當(dāng)中熟悉這些函數(shù),那就來(lái)看一下這個(gè):。請(qǐng)記住,無(wú)論何時(shí)你遇到問(wèn)題,可以畫(huà)一下這些圖,思考一下,看一看這一大串函數(shù),然后繼續(xù)思考。以我個(gè)人經(jīng)驗(yàn),這樣效果很有效。 一旦你開(kāi)始使用了Rx編程,請(qǐng)記住,理解Cold vs Hot Observables的概念是非常必要的,如果你忽視了這一點(diǎn),它就會(huì)反彈回來(lái)并殘忍的反咬你一口。我這里已經(jīng)警告你了,學(xué)習(xí)函數(shù)式編程可以提高你的技能,熟悉一些常見(jiàn)問(wèn)題,例如Rx會(huì)帶來(lái)的副作用 但是響應(yīng)式編程庫(kù)并不僅僅是Rx,還有相對(duì)容易理解的,沒(méi)有Rx那些怪癖的Bacon.js。Elm Language則以它自己的方式支持響應(yīng)式編程:它是一門會(huì)編譯成Javascript + HTML + CSS的響應(yīng)式編程語(yǔ)言,并有一個(gè)time travelling debugger功能,很棒吧。 而Rx對(duì)于像前端和App這樣需要處理大量的編程效果是非常棒的。但是它不只是可以用在客戶端,還可以用在后端或者接近數(shù)據(jù)庫(kù)的地方。事實(shí)上,RxJava就是Netflix服務(wù)端API用來(lái)處理并行的組件。Rx并不是局限于某種應(yīng)用程序或者編程語(yǔ)言的框架,它真的是你編寫(xiě)任何事件驅(qū)動(dòng)程序,可以遵循的一個(gè)非常棒的編程范式。 響應(yīng)式編程就是與異步數(shù)據(jù)流交互的編程范式
--a---b-c---d---X---|->a, b, c, d 是值事件X 是錯(cuò)誤事件| 是完成事件---> 是時(shí)間線(軸)
map
,filter
, scan
等,當(dāng)你調(diào)用這其中的一個(gè)方法時(shí),比如clickStream.map(f)
,它會(huì)返回基于點(diǎn)擊事件流的一個(gè)新事件流。它不會(huì)對(duì)原來(lái)的點(diǎn)擊事件流做任何的修改。這種特性叫做不可變性(immutability),而且它可以和響應(yīng)式事件流搭配在一起使用,就像豆?jié){和油條一樣完美的搭配。這樣我們可以用鏈?zhǔn)胶瘮?shù)的方式來(lái)調(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(+) vvvvvvvvvcounterStream: ---1----2--3----4------5-->
map(f)
函數(shù)會(huì)根據(jù)你提供的f
函數(shù)把原事件流中每一個(gè)返回值分別映射到新的事件流中。在上圖的例子中,我們把每一次點(diǎn)擊事件都映射成數(shù)字1,scan(g)
函數(shù)則把之前映射的值聚集起來(lái),然后根據(jù)x = g(accumulated, current)
算法來(lái)作相應(yīng)的處理,而本例的g
函數(shù)其實(shí)就是簡(jiǎn)單的加法函數(shù)。然后,當(dāng)一個(gè)點(diǎn)擊事件發(fā)生時(shí),counterStream
函數(shù)則上報(bào)當(dāng)前點(diǎn)擊事件總數(shù)。buffer(stream.throttle(250ms))
所做的事情,當(dāng)前我們先不要急著去理解細(xì)節(jié),我們只需專注響應(yīng)式的部分先)。現(xiàn)在我們得到的是多個(gè)含有事件流的列表,然后我們使用了map()
中的函數(shù)來(lái)算出每一個(gè)列表長(zhǎng)度的整數(shù)數(shù)值映射到下一個(gè)事件流當(dāng)中。最后我們使用了過(guò)濾filter(x >= 2)
函數(shù)忽略掉了小于1
的整數(shù)。就這樣,我們用了3步操作生成了我們想要的事件流,接下來(lái),我們就可以訂閱("監(jiān)聽(tīng)")這個(gè)事件并作出我們想要的操作了。"我為什么要采用響應(yīng)式編程?"
以響應(yīng)式編程方式思考的例子
實(shí)現(xiàn)一個(gè)推薦關(guān)注(Who to follow)的功能
--a------|->a就是字符串:'https://api.github.com/users'
var requestStream = Rx.Observable.just('https://api.github.com/users');
requestStream.subscribe(function(requestUrl) { // execute the request jQuery.getJSON(requestUrl, function(responseData) { // ... });}
requestStream.subscribe(function(requestUrl) { // execute the request var responseStream = Rx.Observable.create(function (observer) { jQuery.getJSON(requestUrl) .done(function(response) { observer.onNext(response); }) .fail(function(jqXHR, status, error) { observer.onError(error); }) .always(function() { observer.onCompleted(); }); }); responseStream.subscribe(function(response) { // do something with the response });}
Rx.Observable.create()
操作就是在創(chuàng)建自己定制的事件流,且對(duì)于數(shù)據(jù)事件(onNext()
)和錯(cuò)誤事件(onError()
)都會(huì)顯示的通知該事件每一個(gè)觀察者(或訂閱者)。我們做的只是小小的封裝一下jQuery Ajax Promise而已。等等,這是否意味者jQuery Ajax Promise本質(zhì)上就是一個(gè)被觀察者呢(Observable)?var stream = Rx.Observable.fromPromise(promise)
,就可以很輕松的將Promise轉(zhuǎn)換成一個(gè)被觀察者(Observable),非常簡(jiǎn)單的操作就能讓我們現(xiàn)在就開(kāi)始使用它。不同的是,這些被觀察者都不能兼容Promises/A+,但理論上并不沖突。一個(gè)Promise就是一個(gè)只有一個(gè)返回值的簡(jiǎn)單的被觀察者,而Rx就遠(yuǎn)超于Promise,它允許多個(gè)值返回。subscribe()
方法,這有點(diǎn)類似于回調(diào)地獄(callback hell),而且responseStream
的創(chuàng)建也是依賴于requestStream
的。在之前我們說(shuō)過(guò),在Rx里,有很多很簡(jiǎn)單的機(jī)制來(lái)從其他事件流的轉(zhuǎn)化并創(chuàng)建出一些新的事件流,那么,我們也應(yīng)該這樣做試試。map(f)
,它可以從事件流A中取出每一個(gè)值,并對(duì)每一個(gè)值執(zhí)行f()
函數(shù),然后將產(chǎn)生的新值填充到事件流B。如果將它應(yīng)用到我們的請(qǐng)求和響應(yīng)事件流當(dāng)中,那我們就可以將請(qǐng)求的URL映射到一個(gè)響應(yīng)Promises上了(偽裝成數(shù)據(jù)流)。var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
requestStream: --a-----b--c------------|->responseStream: -----A--------B-----C---|->(小寫(xiě)的是請(qǐng)求事件流, 大寫(xiě)的是響應(yīng)事件流)
responseStream.subscribe(function(response) { // render `response` to the DOM however you wish});
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) { // render `response` to the DOM however you wish});
刷新按鈕
var refreshButton = document.querySelector('.refresh');var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return 'https://api.github.com/users?since=' + randomOffset; });
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()
方法來(lái)實(shí)現(xiàn)。下圖可以解釋merge()
函數(shù)的用處:stream A: ---a--------e-----o----->stream B: -----B---C-----D--------> vvvvvvvvv merge vvvvvvvvv ---a-B---C--e--D--o----->
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()
函數(shù)做的事和你預(yù)期的完全一樣。無(wú)論你的輸入事件流是怎樣的,使用startWith(x)
函數(shù)處理過(guò)后輸出的事件流一定是一個(gè)x
開(kāi)頭的結(jié)果。但是我沒(méi)有總是重復(fù)代碼( DRY),我只是在重復(fù)API的URL字符串,改進(jìn)的方法是將startWith()
函數(shù)挪到refreshClickStream
那里,這樣就可以在啟動(dòng)時(shí),模擬一個(gè)刷新按鈕的點(diǎn)擊事件了。var requestStream = refreshClickStream.startWith('startup click') .map(function() { var randomOffset = Math.floor(Math.random()*500); return 'https://api.github.com/users?since=' + randomOffset; });
startWith()
函數(shù)而已。用事件流將3個(gè)推薦的用戶數(shù)據(jù)模型化
subscribe()
)函數(shù)發(fā)生的渲染步驟里,我們只是稍微提及了一下推薦關(guān)注的UI。現(xiàn)在有了刷新按鈕,我們就會(huì)出現(xiàn)一個(gè)問(wèn)題:當(dāng)你點(diǎn)擊了刷新按鈕,當(dāng)前的三個(gè)推薦關(guān)注用戶沒(méi)有被清楚,而只要響應(yīng)的數(shù)據(jù)達(dá)到后我們就拿到了新的推薦關(guān)注的用戶數(shù)據(jù),為了讓UI看起來(lái)更漂亮,我們需要在點(diǎn)擊刷新按鈕的事件發(fā)生的時(shí)候清楚當(dāng)前的三個(gè)推薦關(guān)注的用戶。refreshClickStream.subscribe(function() { // clear the 3 suggestion DOM elements });
responseStream.subscribe()
),這看起來(lái)并不符合關(guān)注分離(Separation of concerns)原則,還記得響應(yīng)式編程的原則么?var suggestion1Stream = responseStream .map(function(listUsers) { // get one random user from the list return listUsers[Math.floor(Math.random()*listUsers.length)]; });
suggestion2Stream
和推薦關(guān)注的3號(hào)用戶數(shù)據(jù)的事件流suggestion3Stream
都可以方便的從suggestion1Stream
復(fù)制粘貼就好。這里并不是重復(fù)代碼,只是為讓我們的示例更加簡(jiǎn)單,而且我認(rèn)為這是一個(gè)思考如何避免重復(fù)代碼的好案例。suggestion1Stream.subscribe(function(suggestion) { // render the 1st suggestion to the DOM});
suggestion1Stream.subscribe(function(suggestion) { // render the 1st suggestion to the DOM});
null
suggestion data),并且在suggestion1Stream
中包含進(jìn)來(lái),如下:var suggestion1Stream = responseStream .map(function(listUsers) { // get one random user from the list return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) );
null
解釋為"沒(méi)有數(shù)據(jù)",然后把UI元素隱藏起來(lái)。suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // hide the first suggestion DOM element } else { // show the first suggestion DOM element // and render the data }});
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
var suggestion1Stream = responseStream .map(function(listUsers) { // get one random user from the list return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);
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ù)(responses)
var close1Button = document.querySelector('.close1');var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');// and the same for close2Button and close3Buttonvar requestStream = refreshClickStream.startWith('startup click') .merge(close1ClickStream) // we added this .map(function() { var randomOffset = Math.floor(Math.random()*500); return 'https://api.github.com/users?since=' + randomOffset; });
responseStream
函數(shù)來(lái)從響應(yīng)列表里隨機(jī)的抽出一個(gè)用戶數(shù)據(jù)來(lái),就像下面這樣: requestStream: --r---------------> responseStream: ------R----------->close1ClickStream: ------------c----->suggestion1Stream: ------s-----s----->
combineLatest
,應(yīng)該是我們需要的。這個(gè)函數(shù)會(huì)把數(shù)據(jù)流A和數(shù)據(jù)流B作為輸入,并且無(wú)論哪一個(gè)數(shù)據(jù)流發(fā)出一個(gè)值了,combineLatest
函數(shù)就會(huì)將從兩個(gè)數(shù)據(jù)流最近發(fā)出的值a
和b
作為f
函數(shù)的輸入,計(jì)算后返回一個(gè)輸出值(c = f(x,y)
),下面的圖表會(huì)讓這個(gè)函數(shù)的過(guò)程看起來(lái)會(huì)更加清晰:stream A: --a-----------e--------i-------->stream B: -----b----c--------d-------q----> vvvvvvvv combineLatest(f) vvvvvvv ----AB---AC--EC---ED--ID--IQ---->f是轉(zhuǎn)換成大寫(xiě)的函數(shù)
combineLatest()
函數(shù)用在close1ClickStream
和 responseStream
上了,只要關(guān)閉按鈕被點(diǎn)擊,我們就可以獲得最近的響應(yīng)數(shù)據(jù),并在suggestion1Stream
上產(chǎn)生出一個(gè)新值。另一方面,combineLatest()
函數(shù)也是相對(duì)的:每當(dāng)在responseStream
上發(fā)出一個(gè)新的響應(yīng),它將會(huì)結(jié)合一次新的點(diǎn)擊關(guān)閉按鈕事件
來(lái)產(chǎn)生一個(gè)新的推薦關(guān)注的用戶數(shù)據(jù),這非常有趣,因?yàn)樗梢越o我們的suggestion1Stream
簡(jiǎn)化代碼:var suggestion1Stream = close1ClickStream .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);
combineLatest()
函數(shù)使用了最近的兩個(gè)數(shù)據(jù)源,但是如果某一個(gè)數(shù)據(jù)源還沒(méi)有發(fā)出任何東西,combineLatest()
函數(shù)就不能在輸出流上產(chǎn)生一個(gè)數(shù)據(jù)事件。如果你看了上面的ASCII圖表(文章中第一個(gè)圖表),你會(huì)明白當(dāng)?shù)谝粋€(gè)數(shù)據(jù)流發(fā)出一個(gè)值a
時(shí)并沒(méi)有任何的輸出,只有當(dāng)?shù)诙€(gè)數(shù)據(jù)流發(fā)出一個(gè)值b
的時(shí)候才會(huì)產(chǎn)生一個(gè)輸出值。var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this .combineLatest(responseStream, function(click, listUsers) {l return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);
封裝起來(lái)
var refreshButton = document.querySelector('.refresh');var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');var closeButton1 = document.querySelector('.close1');var close1ClickStream = Rx.Observable.fromEvent(closeButton1, 'click');// and the same logic for close2 and close3var 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);// and the same logic for suggestion2Stream and suggestion3Streamsuggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // hide the first suggestion DOM element } else { // show the first suggestion DOM element // and render the data }});
suggestion1Stream
是'close 1'事件結(jié)合從最新的響應(yīng)數(shù)據(jù)中拿到的一個(gè)用戶數(shù)據(jù)的數(shù)據(jù)流,除此之外,當(dāng)刷新事件發(fā)生時(shí)和程序啟動(dòng)時(shí),它就是null
。if
, for
, while
等流程控制語(yǔ)句,或者像JavaScript那樣典型的基于回調(diào)(callback-based)的流程控制。如果可以的話(稍候會(huì)給你留一些實(shí)現(xiàn)細(xì)節(jié)來(lái)作為練習(xí)),你甚至可以在subscribe()
上使用 filter()
函數(shù)來(lái)擺脫if
和else
。在Rx里,我們有例如: map
, filter
, scan
, merge
, combineLatest
, startWith
等數(shù)據(jù)流的函數(shù),還有很多函數(shù)可以用來(lái)控制事件驅(qū)動(dòng)編程(event-driven program)的流程。這些函數(shù)的集合可以讓你使用更少的代碼實(shí)現(xiàn)更強(qiáng)大的功能。接下來(lái)
聯(lián)系客服