摘要:是針對(duì)異步數(shù)據(jù)流的編程。所以這個(gè)數(shù)據(jù)流只包含一個(gè)簡(jiǎn)單的反射值。且慢,天生就是處理異步數(shù)據(jù)流的,為何不把請(qǐng)求的響應(yīng)作為一個(gè)攜帶數(shù)據(jù)的流呢么么噠,概念上沒(méi)有問(wèn)題,我們就來(lái)操作一下。你需要明確通知觀察者或者訂閱者數(shù)據(jù)流的到達(dá)或者錯(cuò)誤的發(fā)生。
"Reactive Programming是神馬?"
互聯(lián)網(wǎng)上有很多不是很友好的解釋。維基百科 寬泛而玄乎。 Stackoverflow教科書(shū)式的解釋非常不適合信任Reactive Manifesto 聽(tīng)起來(lái)像是給給項(xiàng)目經(jīng)理或者是銷售的匯報(bào)。 微軟的 Rx 定義 "Rx = Observables + LINQ + Schedulers" 太重并且太微軟化了,讓人看起來(lái)不知所云?!绊憫?yīng)”、“變化發(fā)生”這些術(shù)語(yǔ)無(wú)法很好地闡釋Reactive Programming的顯著特點(diǎn),聽(tīng)起來(lái)和你熟悉的MV*、編程語(yǔ)言差別不大。 當(dāng)然,我的視角也是基于模型和變換的,要是脫離了這些概念,一切都是無(wú)稽之談了。
那么我要開(kāi)始吧啦吧啦了,(后文中,將使用RP代替Reactive Programming,私底下譯者將Reactive Programming,翻譯為響應(yīng)式編程)。
RP 是針對(duì)異步數(shù)據(jù)流的編程。
一定程度而言,RP并不算新的概念。Event Bus、點(diǎn)擊事件都是異步流。開(kāi)發(fā)者可以觀測(cè)這些異步流,并調(diào)用特定的邏輯對(duì)它們進(jìn)行處理。使用Reactive如同開(kāi)掛:你可以創(chuàng)建點(diǎn)擊、懸停之類的任意流。通常流廉價(jià)(點(diǎn)擊一下就出來(lái)一個(gè))而無(wú)處不在,種類豐富多樣:變量,用戶輸入,屬性,緩存,數(shù)據(jù)結(jié)構(gòu)等等都可以產(chǎn)生流。舉例來(lái)說(shuō):微博回文(譯者注:比如你關(guān)注的微博更新了)和點(diǎn)擊事件都是流:你可以監(jiān)聽(tīng)流并調(diào)用特定的邏輯對(duì)它們進(jìn)行處理。
基于流的概念,RP賦予了你一系列神奇的函數(shù)工具集,使用他們可以合并、創(chuàng)建、過(guò)濾這些流。 一個(gè)流或者一系列流可以作為另一個(gè)流的輸入。你可以 合并 兩個(gè)流,從一堆流中 過(guò)濾 你真正感興趣的那一些,將值從一個(gè)流 映射 到另一個(gè)流。
如果流是RP的核心,我們不妨從“點(diǎn)擊頁(yè)面中的按鈕”這個(gè)熟悉的場(chǎng)景詳細(xì)地了解它。
流是包含了有時(shí)序,正在進(jìn)行事件的序列,可以發(fā)射(emmit)值(某種類型)、錯(cuò)誤、完成信號(hào)。流在包含按鈕的瀏覽器窗口被關(guān)閉時(shí)發(fā)出完成信號(hào)。
我們異步地捕獲發(fā)射的事件,定義一系列函數(shù)在值被發(fā)射后,在錯(cuò)誤被發(fā)射后,在完成信號(hào)被發(fā)射后執(zhí)行。有時(shí),我們忽略對(duì)錯(cuò)誤,完成信號(hào)地處理,僅僅關(guān)注對(duì)值的處理。對(duì)流進(jìn)行監(jiān)聽(tīng),通常稱為訂閱,處理流的函數(shù)是觀測(cè)者,流是被觀測(cè)的主體。這就是觀測(cè)者設(shè)計(jì)模式。
教程中,我們有時(shí)會(huì)使用ASCII字符來(lái)繪制圖表:
--a---b-c---d---X---|-> a, b, c, d 是數(shù)據(jù)流發(fā)射的值 X 是數(shù)據(jù)流發(fā)射的錯(cuò)誤 | 是完成信號(hào) ---> 是時(shí)序軸
嗶嗶完了,我們來(lái)點(diǎn)新的,不然很快你就感覺(jué)到寂寞了。我們將把原來(lái)的點(diǎn)擊事件流轉(zhuǎn)換為新的點(diǎn)擊事件流。
首先我們創(chuàng)建一個(gè)計(jì)數(shù)流來(lái)表明按鈕被點(diǎn)擊的次數(shù)。在RP中,每一個(gè)流都擁有一系列方法,例如map,filter,scan 等等。當(dāng)你在流上調(diào)用這些方法,例如clickStream.map(f),會(huì)返回基于點(diǎn)擊事件流的新的流,同時(shí)原來(lái)的點(diǎn)擊事件流并不會(huì)被改變,這個(gè)特性被稱為不可變性(immutability)。不可變性與RP配合相得益彰,如同美酒加咖啡。我們可以鏈?zhǔn)降卣{(diào)用他們:clickStream.map(f).scan(g)
clickStream: ---c----c--c----c------c---> vvvvv map(c becomes 1) vvvvv ---1----1--1----1------1---> vvvvvvvvv scan(+) vvvvvvvvv counterStream: ---1----2--3----4------5--->
map(f) 函數(shù)對(duì)原來(lái)的流使用我們出入的f函數(shù)進(jìn)行轉(zhuǎn)換,并生成新的流。在上面的例子中,我們將每一次點(diǎn)擊映射為數(shù)字1。scan(g)函數(shù)將所有流產(chǎn)生的值進(jìn)行匯總,通過(guò)傳入x = g(accumulated, current)函數(shù)產(chǎn)生新的值,g 是簡(jiǎn)單的求和函數(shù)。最后 counterStream在點(diǎn)擊發(fā)生后發(fā)射點(diǎn)擊事件發(fā)生的總數(shù)。
為了展示Reactive的真正力量,我們舉個(gè)例子:你想要“兩次點(diǎn)擊”事件的流,或者是“三次點(diǎn)擊”,或者是n次點(diǎn)擊的流。深呼吸一下,試著想想怎么用傳統(tǒng)的命令、狀態(tài)式方法來(lái)解決。我打賭這個(gè)這會(huì)相當(dāng)操蛋,你會(huì)搞些變量來(lái)記錄狀態(tài),還要搞些處理時(shí)延的機(jī)制。
如果用RP來(lái)解決,太他媽簡(jiǎn)單了。實(shí)際上4行代碼就可以搞定。先不要看代碼,不管你是菜鳥(niǎo)還是牛逼,使用圖表來(lái)思考可以使你更好地理解構(gòu)建這些流的方法。
灰色框里面的函數(shù)會(huì)把一個(gè)流轉(zhuǎn)換成另外一個(gè)流。首先我們把點(diǎn)擊打包到list中,如果點(diǎn)擊后消停了250毫秒,我們就重新打包一個(gè)新的list(顯然buffer(stream.throttle(250ms))就是用來(lái)干這個(gè)的,不明白細(xì)節(jié)沒(méi)有關(guān)系,反正是demo嘛)。我們?cè)诹斜砩险{(diào)用map(),將列表的長(zhǎng)度映射為一個(gè)整數(shù)的流。最后,我們通過(guò)filter(x >= 2)過(guò)濾掉整數(shù)1。哈哈:3個(gè)操作就生成了我們需要的流,現(xiàn)在我們可以訂閱(監(jiān)聽(tīng))這個(gè)流,然后來(lái)完成我們需要的邏輯了。
通過(guò)這個(gè)例子,我希望你能感受到使用RP的牛逼之處了。這僅僅是冰山一角。你可以在不同地流上(比如API響應(yīng)的流)進(jìn)行同樣的操作。同時(shí),Reactive還提供了許多其他實(shí)用的函數(shù)。
"我要在今后采用RP范式進(jìn)行編程嗎?"RP 提高了編碼的抽象程度,你可以更好地關(guān)注在商業(yè)邏輯中各種事件的聯(lián)系避免大量細(xì)節(jié)而瑣碎的實(shí)現(xiàn),使得編碼更加簡(jiǎn)潔。
使用RP,將使得數(shù)據(jù)、交互錯(cuò)綜復(fù)雜的web、移動(dòng)app開(kāi)發(fā)收益更多。10年以前,與網(wǎng)頁(yè)的交互僅僅是提交表單、然后根據(jù)服務(wù)器簡(jiǎn)單地渲染返回結(jié)果這些事情。App進(jìn)化得越來(lái)越有實(shí)時(shí)性:修改表單中一個(gè)域可以同步地更新到后端服務(wù)器。“點(diǎn)贊”信息實(shí)時(shí)地在不同用戶設(shè)備上同步。
現(xiàn)代App中大量的實(shí)時(shí)事件創(chuàng)造了更好的交互和用戶體驗(yàn),披荊斬棘需要利劍在手,RP就是你手中的利劍。
通過(guò)實(shí)例RP編程思想我們將從實(shí)例可以深入RP的編程思想,文章末尾,一個(gè)完整地實(shí)例應(yīng)用會(huì)被構(gòu)建,你也會(huì)理解整個(gè)過(guò)程。
我選擇 JavaScript 和 RxJS 作為實(shí)例的構(gòu)建工具。因?yàn)榇蠖嚅_(kāi)發(fā)者都熟悉JavaScript語(yǔ)言。Rx* library family 在各種語(yǔ)言和平臺(tái)都是實(shí)現(xiàn) (.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy, 等等)。無(wú)論你選擇在哪個(gè)平臺(tái)或者那種語(yǔ)言實(shí)踐RP,你都將從本教程中受益。(譯者注:Rx,即ReactiveX,其中X代表不同的語(yǔ)言和技術(shù)棧,比如.NET,Java,Scala,Ruby,Javascript。RxJS表示RP基于Javascript語(yǔ)言的實(shí)現(xiàn)。后文中Rx代表所有實(shí)現(xiàn)了RP的特定技術(shù)棧)
微博(Twitter)簡(jiǎn)易版“你可能感興趣的人”微博主頁(yè),有一個(gè)組件會(huì)推薦給你那些你可能感興趣的人。
我們的Demo將使用這個(gè)場(chǎng)景,關(guān)注下面這些主要特性:
頁(yè)面打開(kāi)后,通過(guò)API加載數(shù)據(jù)展示3個(gè)你可能感興趣的用戶賬號(hào)
點(diǎn)擊“刷新”按鈕,重新加載三個(gè)新的用戶賬號(hào)
在一個(gè)用戶賬號(hào)上點(diǎn)擊"x" 按鈕,清除當(dāng)前這個(gè)賬戶,重新加載一個(gè)新的賬戶
每行展示賬戶的信息和這個(gè)賬戶主頁(yè)的鏈接
其他特性和按鈕我們暫且忽略,由于Twitter在最近關(guān)閉了公共API授權(quán)接口,我們選擇Github作為代替,展示GitHub用戶的賬戶。實(shí)例中我們使用該接口獲取GitHub用戶.
如果你希望先睹為快,完成后的代碼已經(jīng)發(fā)布在了Jsfiddle。
"你可能感興趣的用戶"請(qǐng)求&響應(yīng)這個(gè)問(wèn)題使用Rx怎么解?,呵呵,我們從Rx的箴言開(kāi)始: 神馬都是流 。首先我們做最簡(jiǎn)單的部分——頁(yè)面打開(kāi)后通過(guò)API加載3個(gè)賬戶的信息。分三步走:(1)發(fā)一個(gè)請(qǐng)求(2)獲得響應(yīng)(3)依據(jù)響應(yīng)渲染頁(yè)面。那么,我們先使用流來(lái)表示請(qǐng)求。我靠,表示個(gè)請(qǐng)求用得著嗎?不過(guò)千里之行始于足下。
頁(yè)面加載時(shí),僅需要一個(gè)請(qǐng)求。所以這個(gè)數(shù)據(jù)流只包含一個(gè)簡(jiǎn)單的反射值。稍后,我們?cè)傺芯咳绾味鄠€(gè)請(qǐng)求出現(xiàn)的情況,現(xiàn)在先從一個(gè)請(qǐng)求開(kāi)始。
--a------|-> a是字符串 "https://api.github.com/users"
這個(gè)流中包含了我們希望請(qǐng)求的URL地址。一旦這個(gè)請(qǐng)求事件發(fā)生,我們可以獲知兩件事情:請(qǐng)求流發(fā)射值(字符串URL)的時(shí)間就是請(qǐng)求需要被執(zhí)行的時(shí)間,請(qǐng)求需要請(qǐng)求的地址就是請(qǐng)求流發(fā)射的值。
在Rx*中構(gòu)建一個(gè)單值的流很容易。官方術(shù)語(yǔ)中把流稱為“觀察的對(duì)象”("Observable"),因?yàn)榱骺梢员挥^察、訂閱,這么稱呼顯得很蠢,我自己把他們稱為 stream 。
var requestStream = Rx.Observable.just("https://api.github.com/users");
目前這個(gè)攜帶字符串的流沒(méi)有其他操作,我們需要在這個(gè)流發(fā)射值之后,做點(diǎn)什么:通過(guò)訂閱 這個(gè)流來(lái)實(shí)現(xiàn)。
requestStream.subscribe(function(requestUrl) { // 執(zhí)行異步請(qǐng)求 jQuery.getJSON(requestUrl, function(responseData) { // ... }); }
我們采用了jQuery的Ajax回調(diào) (假設(shè)讀著已經(jīng)了解jQuery ajax回調(diào)) 來(lái)處理異步請(qǐng)求操作。 且慢,Rx天生就是處理異步 數(shù)據(jù)流的,
為何不把請(qǐng)求的響應(yīng)作為一個(gè)攜帶數(shù)據(jù)的流呢? 么么噠,概念上沒(méi)有問(wèn)題,我們就來(lái)操作一下。
requestStream.subscribe(function(requestUrl) { // 執(zhí)行異步請(qǐng)求 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) { // 業(yè)務(wù)邏輯 }); }
使用Rx.Observable.create()方法可以自定義你需要的流。你需要明確通知觀察者(或者訂閱者)數(shù)據(jù)流的到達(dá)(onNext()) 或者錯(cuò)誤的發(fā)生(onError())。這個(gè)實(shí)現(xiàn)中,我們封裝了jQuery 的異步 Promise。那么Promise也是可觀察對(duì)象嗎?
冰狗,你猜對(duì)啦!
可觀察對(duì)象(Observable)是超級(jí)Promise(原文Promise++,可以對(duì)比C,C++,C++在兼容C的同時(shí)引入了面向?qū)ο蟮忍匦?。 在Rx環(huán)境中,你可以簡(jiǎn)單的通過(guò)var stream = Rx.Observable.fromPromise(promise)將Promise轉(zhuǎn)換為可觀察對(duì)象, 我們后面將這樣使用, 唯一的區(qū)別是,可觀察對(duì)象與Promises/A+ 并不兼容, 但是理論上不會(huì)產(chǎn)生沖突。 Promise 可以看做只能發(fā)射單值的可觀察對(duì)象,Rx流則允許返回多個(gè)值。
不過(guò),可觀察對(duì)象至少和Promise一樣強(qiáng)大。如果你相信針對(duì)Promise的那些吹捧,不妨也留意一下Rx環(huán)境中的可觀察對(duì)象。
回到我們的例子,細(xì)心的你肯定看到了subscribe()的嵌套使用,這和回調(diào)函數(shù)嵌套一樣令人惱火。responseStream 的確和 requestStream 存在依賴關(guān)系。前面我們不是提到過(guò)Rx有一些牛逼的工具集嗎?在Rx中我們擁有簡(jiǎn)單的機(jī)制把一個(gè)流轉(zhuǎn)化為一個(gè)新的流,我們不妨試試。
我們先介紹 map(f)函數(shù)。該函數(shù)在流A的每個(gè)之上調(diào)用函數(shù)f() , 然后在流B上生成對(duì)應(yīng)的新值。如果在請(qǐng)求、響應(yīng)流上調(diào)用map(f),我們可以將請(qǐng)求的URL隱射為響應(yīng)流中的Promise(此時(shí)響應(yīng)流中包含了Promise的序列)。
var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
我們把上面代碼執(zhí)行后的返回結(jié)果稱為 metastream (譯者注:按字面可以翻譯為“元流”,即包含流的流。類似概念例如:元編程——用于生成程序的編程方法;元知識(shí)——獲取知識(shí)的知識(shí)):包含其他流的流。沒(méi)什么嚇人的, 一個(gè)metastream會(huì)在執(zhí)行后發(fā)射一個(gè)流。 你可以把它看做一個(gè)指針 指針): 每一個(gè)發(fā)射的值是指向另外一個(gè)流的 指針 。在我們的例子中,每一個(gè)URL被映射為一個(gè)指向Promise流的指針,每一個(gè)Promise流中包含了相應(yīng)的響應(yīng)信息。
(譯者注:以下給出 metastream 的方法的解析方法,方便與下面的方法進(jìn)行對(duì)比):
responseMetastream.subscribe(function(streamedPromise) { // 首先展開(kāi)metastream,獲取內(nèi)部的流 streamedPromise.subscribe(function(responseJsonObject) { // 返回內(nèi)部流發(fā)射的值 return responseJsonObject; }); });
當(dāng)前版本響應(yīng)產(chǎn)生的metastream看起來(lái)有些讓人疑惑,似乎用處不大。當(dāng)前場(chǎng)景中,我們僅僅需要獲得簡(jiǎn)單的響應(yīng)流,流中發(fā)射的值為簡(jiǎn)單的JSON對(duì)象。使用flatMap:這個(gè)函數(shù)可以將枝干的流的值發(fā)射到主干流之上。當(dāng)然metastream的產(chǎn)生并不是bug,只是這個(gè)場(chǎng)景不適合而已,map(),flatMap()都是Rx處理異步請(qǐng)求工具中的一部分。(譯者注:如果流A中包含了若干其他流,在流A上調(diào)用flatMap()函數(shù),將會(huì)發(fā)射其他流的值,并將發(fā)射的所有值組合生成新的流。)
var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
贊!響應(yīng)流是依照請(qǐng)求流定義的,如果 場(chǎng)景中生成了更多的請(qǐng)求流,我們也會(huì)生成同樣多的響應(yīng)流:
請(qǐng)求流: --a-----b--c------------|-> 響應(yīng)流: -----A--------B-----C---|-> (小寫(xiě)字母表示請(qǐng)求, 大寫(xiě)字母代表響應(yīng))
獲得響應(yīng)流之后,我們就可以再訂閱后渲染頁(yè)面了:
responseStream.subscribe(function(response) { // 在瀏覽器中渲染響應(yīng)數(shù)據(jù)的邏輯 });
馬克一下目前的代碼:
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) { // 在瀏覽器中渲染響應(yīng)數(shù)據(jù)的邏輯 });刷新“你可能感興趣的用戶”
忘了說(shuō)了,我們每一次請(qǐng)求都會(huì)返回100個(gè)GitHub用戶的數(shù)據(jù)。GitHub的API只允許我們?cè)O(shè)置頁(yè)面的偏移量但是不能設(shè)置每次獲得數(shù)據(jù)的數(shù)量。嗯,我們需要3個(gè)推薦用戶的數(shù)據(jù),其他97個(gè)就這樣浪費(fèi)了。暫時(shí)忽略這個(gè)問(wèn)題,后面我們看看怎么緩存數(shù)據(jù)來(lái)減少數(shù)據(jù)的浪費(fèi)。
每一次點(diǎn)擊刷新按鈕(高能注意:是一個(gè)按鈕,點(diǎn)擊后刷新“我可能感興趣的人”的數(shù)據(jù),而不是瀏覽器的刷新按鈕),請(qǐng)求流都會(huì)發(fā)射新的URL值,我們以此獲得新的響應(yīng)。刷新分為兩步:產(chǎn)生一個(gè)刷新按鈕被點(diǎn)擊的事件流(RP箴言:神馬都是流);訂閱刷新事件流后改變請(qǐng)求流的URL地址。RxJS提供了工具方便我們將時(shí)間監(jiān)聽(tīng)器轉(zhuǎn)換為可觀察對(duì)象。
var refreshButton = document.querySelector(".refresh"); var refreshClickStream = Rx.Observable.fromEvent(refreshButton, "click");
因?yàn)辄c(diǎn)擊刷新事件并不會(huì)攜帶需要請(qǐng)求的API的URL,我們需要把每一次點(diǎn)擊映射到真正的URL之上。具體實(shí)現(xiàn)方式是,在刷新點(diǎn)擊流發(fā)生后,我們通過(guò)產(chǎn)生隨機(jī)的頁(yè)面拼湊出URL,并向GitHub發(fā)起請(qǐng)求。
var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return "https://api.github.com/users?since=" + randomOffset; });
由于是簡(jiǎn)單的教程,我并沒(méi)有寫(xiě)相關(guān)的測(cè)試,但是我仍然知道原先的功能被我搞砸啦。呃。。。頁(yè)面打開(kāi)后居然沒(méi)有請(qǐng)求流了,除非我點(diǎn)擊刷新按鈕,否則數(shù)據(jù)怎么都出不來(lái)。擦。。。我希望 不管 是點(diǎn)擊刷新按鈕"_還是_"第一次打開(kāi)頁(yè)面,都可以產(chǎn)生獲得“我可能感興趣的人”的數(shù)據(jù)的GitHub的請(qǐng)求流。
把兩個(gè)流分開(kāi)寫(xiě)特別簡(jiǎn)單,我們已經(jī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");
但是我們?cè)趺窗褍蓚€(gè)流“合并”在一塊呢?使用 merge()函數(shù)吧。我們用ASCII圖表來(lái)解釋這個(gè)函數(shù)的作用:
流 A: ---a--------e-----o-----> 流 B: -----B---C-----D--------> vvvvvvvvv merge vvvvvvvvv ---a-B---C--e--D--o----->
使用merge()后簡(jiǎn)單多了:
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 );
如果不需要requestOnRefreshStream、startupRequestStream這兩個(gè)中間流,寫(xiě)法更干凈、簡(jiǎn)潔。
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"));
還能更簡(jiǎn)單,更有可讀性:
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ù)的作用和它的命名一樣。 無(wú)論是什么樣的流,startWith(x) 都會(huì)把x作為這個(gè)流的啟示輸入并發(fā)射出來(lái)。 上面的實(shí)現(xiàn),還不夠DRY(Don"t repeat yourself,不要重復(fù)!),API請(qǐng)求的URL地址重復(fù)了兩遍。我們將 startWith() 緊接在refreshClickStream之后,在頁(yè)面打開(kāi)后就模擬一次點(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; });
Nice!事情不會(huì)被搞砸了,startWith()完美解決了問(wèn)題。
3位“你可能感興趣的用戶”的流的構(gòu)建目前為止,僅僅在訂閱(subscribe())時(shí),你會(huì)觸及到“感興趣的用戶”區(qū)塊的渲染。但是通過(guò)刷新按鈕,問(wèn)題接踵而至:你點(diǎn)擊了刷新按鈕,在新的響應(yīng)到達(dá)之前,原來(lái)的“你可能感興趣的”3個(gè)用戶并不會(huì)馬上消失。為了增強(qiáng)用戶體驗(yàn),我們希望在用戶點(diǎn)擊了刷新按鈕后就清楚老數(shù)據(jù)。
refreshClickStream.subscribe(function() { // 清楚舊數(shù)據(jù): 3個(gè)你可能感興趣的用戶的DOM元素 });
停!不要用力過(guò)猛。兩個(gè) 訂閱行為都會(huì)影響到這個(gè)區(qū)塊的渲染。(responseStream.subscribe()、refreshClickStream.subscribe()),并且上面的設(shè)計(jì)也不符合關(guān)注分離的理念。還記得RP 神馬都是流 的箴言嗎?
那么開(kāi)始構(gòu)建這個(gè)專門的推薦流:流會(huì)發(fā)射“你可能感興趣的用戶”的JSON對(duì)象。我們會(huì)分別構(gòu)建三種這樣的流,第一種長(zhǎng)這個(gè)樣:
var suggestion1Stream = responseStream .map(function(listUsers) { // 隨機(jī)從列表中取出一個(gè)用戶 return listUsers[Math.floor(Math.random()*listUsers.length)]; });
另外兩個(gè)流suggestion2Stream 和 suggestion3Stream復(fù)制粘貼就好啦。呃。。。DRY不要重復(fù),我把這個(gè)問(wèn)題作為這個(gè)教程的聯(lián)系,自己做一遍你會(huì)去思考這類場(chǎng)景中如何避免代碼的重復(fù)。
譯者注:如果使用UnderScore,一種方法是,新的方法總是會(huì)返回JSON Object數(shù)組:
var suggestionStream = responseStream .map(suggestionN(listUsers, n)); function suggestionN(listUsers, n) { _.times(n, function() { return listUsers[Math.floor(Math.random()*listUsers.length)]; }) }
我們不再訂閱響應(yīng)流,而是變更為:
suggestion1Stream.subscribe(function(suggestion) { // 在區(qū)塊中渲染1位用戶的DOM元素 });
回到原始需求:“每一次刷新后,清除原來(lái)的用戶”,我們可以在刷新后,返回null作為推薦流:
var suggestion1Stream = responseStream .map(function(listUsers) { // 隨機(jī)從列表中取出一個(gè)用戶 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) );
在渲染環(huán)節(jié),null代表無(wú)數(shù)據(jù),我們就隱藏之前的DOM元素。
suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 在區(qū)塊中隱藏一個(gè)推薦用戶的DOM元素 } else { // 在區(qū)塊中渲染一個(gè)推薦用戶的DOM元素 } });
整個(gè)事件流如圖所示:
刷新按鈕流: ----------o--------o----> 請(qǐng)求流: -r--------r--------r----> 響應(yīng)流: ----R---------R------R--> 推薦1個(gè)用戶: ----s-----N---s----N-s-->
N 表示 null.
頁(yè)面打開(kāi)后,我們渲染“空”推薦區(qū)塊,可以通過(guò)在推薦流中附加startWith(null)實(shí)現(xiàn):
var suggestion1Stream = responseStream .map(function(listUsers) { // 隨機(jī)從列表中取出一個(gè)用戶 return listUsers[Math.floor(Math.random()*listUsers.length)]; }) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);
Which results in:
刷新按鈕流: ----------o---------o----> 請(qǐng)求流: -r--------r---------r----> 響應(yīng)流: ----R----------R------R--> 推薦1個(gè)用戶: -N--s-----N----s----N-s-->關(guān)閉一個(gè)推薦元素,從緩存獲得新的推薦元素
最后一個(gè)需要實(shí)現(xiàn)的功能是:點(diǎn)擊"x"按鈕后關(guān)閉當(dāng)前的推薦元素,載入一個(gè)新的數(shù)據(jù)并渲染。拍腦袋意向,無(wú)論點(diǎn)擊了啥按鈕,我們重新請(qǐng)求一次新數(shù)據(jù),生成一個(gè)新的響應(yīng)流就好了:
var close1Button = document.querySelector(".close1"); var close1ClickStream = Rx.Observable.fromEvent(close1Button, "click"); // close2Button 和 close3Button 作為練習(xí) var requestStream = refreshClickStream.startWith("startup click") .merge(close1ClickStream) // 加上這個(gè) .map(function() { var randomOffset = Math.floor(Math.random()*500); return "https://api.github.com/users?since=" + randomOffset; });
擦,點(diǎn)擊了關(guān)閉按鈕整個(gè)推薦區(qū)塊都被刷新了!看來(lái)我們只有使用原來(lái)的相應(yīng)流才能解決這個(gè)bug,況且每次慷慨大方的GitHub給我們100個(gè)用戶的數(shù)據(jù),我們只使用3個(gè),還有1大堆留著等我們用呢,沒(méi)有必要再請(qǐng)求更多的數(shù)據(jù)了。
讓我們從流的角度思考,當(dāng)點(diǎn)擊"x"事件發(fā)生后,我們使用 最近一次的相應(yīng)流 并從中隨機(jī)取出用戶就好了:
請(qǐng)求流: --r---------------> 響應(yīng)流: ------R-----------> 點(diǎn)擊關(guān)閉流: ------------c-----> 推薦1個(gè)用戶流: ------s-----s----->
在Rx*框架中,一個(gè)使用函數(shù)叫 combineLatest 。 函數(shù)將兩個(gè)流作為輸入,并且當(dāng)其中任意一個(gè)流發(fā)射之后, combineLatest 都會(huì)組合兩個(gè)流中最新的值 a 和 b然后輸出一個(gè)新的流,流的值為 c = f(x,y) 其中 f(x, y) 是傳入的自定義函數(shù),配合上時(shí)序圖更好理解:
流 A: --a-----------e--------i--------> 流 B: -----b----c--------d-------q----> vvvvvvvv combineLatest(f) vvvvvvv ----AB---AC--EC---ED--ID--IQ----> 這里的函數(shù)f,將輸入的字符串變?yōu)榇髮?xiě)
現(xiàn)在我們?cè)?close1ClickStream 和 responseStream使用combineLatest() , 只要用戶點(diǎn)擊關(guān)閉按鈕,我們就結(jié)合最新的響應(yīng)流來(lái)產(chǎn)生suggestion1Stream。 另一個(gè)方面,combineLatest() 是一個(gè)同步操作:每當(dāng)新的響應(yīng)流發(fā)射了值, 同樣會(huì)結(jié)合 close1ClickStream產(chǎn)生新的推薦數(shù)據(jù)。這樣我們大大簡(jiǎn)化了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);
最后還有一點(diǎn)點(diǎn)問(wèn)題:combineLatest()需要結(jié)合傳入的兩個(gè)流,如果其中一個(gè)流從未發(fā)射過(guò)任何值,combineLatest()將不會(huì)輸入任何新的流?;仡櫼幌律厦娴腁SCII圖表,當(dāng)?shù)谝粋€(gè)流發(fā)射值a時(shí),不會(huì)有任何輸出,僅當(dāng)?shù)诙€(gè)流也發(fā)射了值b后,combineLatest()才會(huì)開(kāi)始向外輸出。
解決方法很多,我們采取最簡(jiǎn)單的方式(上面例子也用到過(guò)),我們?cè)陧?yè)面打開(kāi)時(shí)限模擬一次關(guān)閉按鈕的點(diǎn)擊:
var suggestion1Stream = close1ClickStream.startWith("startup click") // we added this .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);總結(jié)
再M(fèi)ark一下當(dā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 作為練習(xí) 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 作為練習(xí) suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // 隱藏一個(gè)用戶的DOM元素 } else { // 渲染一個(gè)新的推薦用戶的DOM元素 } });
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/78467.html
摘要:由于技術(shù)棧的學(xué)習(xí),筆者需要在原來(lái)函數(shù)式編程知識(shí)的基礎(chǔ)上,學(xué)習(xí)的使用。筆者在社區(qū)發(fā)現(xiàn)了一個(gè)非常高質(zhì)量的響應(yīng)式編程系列教程共篇,從基礎(chǔ)概念到實(shí)際應(yīng)用講解的非常詳細(xì),有大量直觀的大理石圖來(lái)輔助理解流的處理,對(duì)培養(yǎng)響應(yīng)式編程的思維方式有很大幫助。 showImg(https://segmentfault.com/img/bVus8n); [TOC] 一. 響應(yīng)式編程 響應(yīng)式編程,也稱為流式編程...
摘要:是一個(gè)基于可觀測(cè)數(shù)據(jù)流在異步編程應(yīng)用中的庫(kù)。正如官網(wǎng)所說(shuō),是基于觀察者模式,迭代器模式和函數(shù)式編程。它具有時(shí)間與事件響應(yīng)的概念。通知不再發(fā)送任何值。和通知可能只會(huì)在執(zhí)行期間發(fā)生一次,并且只會(huì)執(zhí)行其中的一個(gè)。 RxJS是一個(gè)基于可觀測(cè)數(shù)據(jù)流在異步編程應(yīng)用中的庫(kù)。 ReactiveX is a combination of the best ideas fromthe Observer p...
摘要:官網(wǎng)地址聊天機(jī)器人插件開(kāi)發(fā)實(shí)例教程一創(chuàng)建插件在系統(tǒng)技巧使你的更加專業(yè)前端掘金一個(gè)幫你提升技巧的收藏集。我會(huì)簡(jiǎn)單基于的簡(jiǎn)潔視頻播放器組件前端掘金使用和實(shí)現(xiàn)購(gòu)物車場(chǎng)景前端掘金本文是上篇文章的序章,一直想有機(jī)會(huì)再次實(shí)踐下。 2道面試題:輸入U(xiǎn)RL按回車&HTTP2 - 掘金通過(guò)幾輪面試,我發(fā)現(xiàn)真正那種問(wèn)答的技術(shù)面,寫(xiě)一堆項(xiàng)目真不如去刷技術(shù)文章作用大,因此刷了一段時(shí)間的博客和掘金,整理下曾經(jīng)被...
摘要:選擇后,僅有聯(lián)通的可觀察對(duì)象會(huì)被觀察到。從外部看,所有訂閱者僅能觀測(cè)到這個(gè)聯(lián)通了支流。,其中表示輸入流,是操作符,是最后的輸出流。截圖驗(yàn)證一下當(dāng)一個(gè)流被聯(lián)通后,其他的流腫么辦先記住結(jié)論未被選擇的流將被調(diào)用方法,也就是說(shuō),他們被終止了。 起因 在SegmentFault里發(fā)布過(guò)一篇RxJS的簡(jiǎn)明教程,很多人反饋對(duì)這個(gè)主題很是很感興趣,詳見(jiàn)RxJS簡(jiǎn)明教程。 Rx 是一種編程的思維,而不是...
摘要:鏈接教程一安裝和配置教程二登錄頁(yè)制作教程三設(shè)置頁(yè)制作教程四安卓硬件返回鍵處理教程五基本的網(wǎng)絡(luò)請(qǐng)求這是最后一節(jié),本節(jié)主要用最簡(jiǎn)單網(wǎng)絡(luò)請(qǐng)求和基本的內(nèi)置指令做一個(gè)演示。接收數(shù)據(jù)用依賴注入網(wǎng)絡(luò)請(qǐng)求會(huì)返回一個(gè)對(duì)象。 showImg(https://segmentfault.com/img/remote/1460000010805290); 鏈接: ionic3教程(一)安裝和配置 ionic...
摘要:鏈接教程一安裝和配置教程二登錄頁(yè)制作教程三設(shè)置頁(yè)制作教程四安卓硬件返回鍵處理教程五基本的網(wǎng)絡(luò)請(qǐng)求這是最后一節(jié),本節(jié)主要用最簡(jiǎn)單網(wǎng)絡(luò)請(qǐng)求和基本的內(nèi)置指令做一個(gè)演示。接收數(shù)據(jù)用依賴注入網(wǎng)絡(luò)請(qǐng)求會(huì)返回一個(gè)對(duì)象。 showImg(https://segmentfault.com/img/remote/1460000010805290); 鏈接: ionic3教程(一)安裝和配置 ionic...
閱讀 2298·2021-11-24 11:15
閱讀 3184·2021-11-24 10:46
閱讀 1480·2021-11-24 09:39
閱讀 3988·2021-08-18 10:21
閱讀 1538·2019-08-30 15:53
閱讀 1459·2019-08-30 11:19
閱讀 3389·2019-08-29 18:42
閱讀 2423·2019-08-29 16:58