乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      響應(yīng)式編程,是明智的選擇

       loudf 2019-02-17

      相信你們?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)的書,一本只是在給你描繪響應(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)你開始學(xué)習(xí)響應(yīng)式編程。然后看各種庫(kù)文檔才可以給你更多的指引。希望這篇文章能夠幫助你快速地進(jìn)入響應(yīng)式編程的世界。

      "什是響應(yīng)式編程?"

      網(wǎng)絡(luò)上有一大堆糟糕的解釋和定義,如Wikipedia上通常都是些非?;\統(tǒng)和理論性的解釋,而Stackoverflow上的一些規(guī)范的回答顯然也不適合新手來(lái)參考,Reactive Manifesto看起來(lái)也只像是拿給你的PM或者老板看的東西,微軟的Rx術(shù)語(yǔ)"Rx = Observables + LINQ + Schedulers" 也顯得太過(guò)沉重,而且充滿了太多微軟式的東西,反而給我們帶來(lái)更多疑惑。相對(duì)于你使用的MV*框架以及你鐘愛(ài)的編程語(yǔ)言,"Reactive"和"Propagation of change"這樣的術(shù)語(yǔ)并沒(méi)有傳達(dá)任何有意義的概念。當(dāng)然,我的view框架能夠從model做出反應(yīng),我的改變當(dāng)然也會(huì)傳播,如果沒(méi)有這些,我的界面根本就沒(méi)有東西可渲染。

      所以,不要再扯這些廢話了。

      響應(yīng)式編程就是與異步數(shù)據(jù)流交互的編程范式

      一方面,這已經(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)聽這樣的數(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è)按鈕"的事件流開始

      Click event stream

      一個(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)聽這個(gè)事件流的過(guò)程叫做訂閱,我們定義的函數(shù)叫做觀察者,而事件流就可以叫做被觀察的主題(或者叫被觀察者)。你應(yīng)該察覺(jué)到了,對(duì)的,它就是觀察者模式

      上面的示意圖我們也可以用ASCII碼的形式重新畫一遍,請(qǐng)注意,下面的部分教程中我們會(huì)繼續(xù)使用這幅圖:

      --a---b-c---d---X---|->
      
      a, b, c, d 是值事件
      X 是錯(cuò)誤事件
      | 是完成事件
      ---> 是時(shí)間線(軸)
      

      現(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ù),例如 map,filterscan等,當(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(+) vvvvvvvvv
      counterStream: ---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ù)。

      為了展示響應(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è)非常棒的方法。

      多次點(diǎn)擊事件流

      圖中,灰色盒子表示將上面的事件流轉(zhuǎn)換下面的事件流的函數(shù)過(guò)程,首先根據(jù)250毫秒的間隔時(shí)間(event silence, 譯者注:無(wú)事件發(fā)生的時(shí)間段,上一個(gè)事件發(fā)生到下一個(gè)事件發(fā)生的間隔時(shí)間)把點(diǎn)擊事件流一段一隔開,再將每一段的一個(gè)或多個(gè)點(diǎn)擊事件添加到列表中(這就是這個(gè)函數(shù):buffer(stream.throttle(250ms))所做的事情,當(dāng)前我們先不要急著去理解細(xì)節(jié),我們只需專注響應(yīng)式的部分先)?,F(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)聽")這個(gè)事件并作出我們想要的操作了。

      我希望你能感受到這個(gè)示例的優(yōu)雅之處。當(dāng)然了,這個(gè)示例也只是響應(yīng)式編程魔力的冰山一角而已,你同樣可以將這3步操作應(yīng)用到不同種類的事件流中去,例如,一串API響應(yīng)的事件流。另一方面,你還有非常多的函數(shù)可以使用。

      "我為什么要采用響應(yīng)式編程?"

      響應(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)生交互,在開發(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)式編程就正好是我們想要的答案。

      以響應(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)用是非常廣泛的,例如(.NETJavaScalaClojureJavaScriptRubyPythonC++Objective-C/Cocoa,Groovy等等。所以,無(wú)論你用的是什么語(yǔ)言、庫(kù)、工具,你都能從下面這個(gè)教程中學(xué)到東西(從中受益)。

      實(shí)現(xiàn)一個(gè)推薦關(guān)注(Who to follow)的功能

      在Twitter里有一個(gè)UI元素向你推薦你可以關(guān)注的用戶,如下圖:

      Twitter Who to follow suggestions box

      我們將聚焦于模仿它的主要功能,它們是:

      • 開始階段,從API加載推薦關(guān)注的用戶賬戶數(shù)據(jù),然后顯示三個(gè)推薦用戶
      • 點(diǎn)擊刷新,加載另外三個(gè)推薦用戶到當(dāng)前的三行中顯示
      • 點(diǎn)擊每一行的推薦用戶上的'x'按鈕,清楚當(dāng)前被點(diǎn)擊的用戶,并顯示新的一個(gè)用戶到當(dāng)前行
      • 每一行顯示一個(gè)用戶的頭像并且在點(diǎn)擊之后可以鏈接到他們的主頁(yè)。

      我們可以先不管其他的功能和按鈕,因?yàn)樗鼈兪谴我?。因?yàn)門witter最近關(guān)閉了未經(jīng)授權(quán)的公共API調(diào)用,我們將用Github獲取用戶的API代替,并且以此來(lái)構(gòu)建我們的UI。

      如果你想先看一下最終效果,這里有完成后的代碼

      Request和Response

      在Rx中是怎么處理這個(gè)問(wèn)題呢?,在開始之前,我們要明白,(幾乎)一切都可以成為一個(gè)事件流,這就是Rx的準(zhǔn)則(mantra)。讓我們從最簡(jiǎn)單的功能開始:"開始階段,從API加載推薦關(guān)注的用戶賬戶數(shù)據(jù),然后顯示三個(gè)推薦用戶"。其實(shí)這個(gè)功能沒(méi)什么特殊的,簡(jiǎn)單的步驟分為: (1)發(fā)出一個(gè)請(qǐng)求,(2)獲取響應(yīng)數(shù)據(jù),(3)渲染響應(yīng)數(shù)據(jù)。ok,讓我們把請(qǐng)求作為一個(gè)事件流,一開始你可能會(huì)覺(jué)得這樣做有些夸張,但別急,我們也得從最基本的開始,不是嗎?

      開始時(shí)我們只需做一次請(qǐng)求,如果我們把它作為一個(gè)數(shù)據(jù)流的話,它只能成為一個(gè)僅僅返回一個(gè)值的事件流而已。一會(huì)兒我們還會(huì)有很多請(qǐng)求要做,但當(dāng)前,只有一個(gè)。

      --a------|->
      
      a就是字符串:'https://api.github.com/users'
      

      這是一個(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)這名字比較傻,所以我更喜歡把它叫做事件流。

      var requestStream = Rx.Observable.just('https://api.github.com/users');

      但現(xiàn)在,這只是一個(gè)字符串的事件流而已,并沒(méi)有做其他操作,所以我們需要在發(fā)出這個(gè)值的時(shí)候做一些我們要做的操作,可以通過(guò)訂閱(subscribing)這個(gè)事件來(lái)實(shí)現(xiàn)。

      requestStream.subscribe(function(requestUrl) {
        // execute the request
        jQuery.getJSON(requestUrl, function(responseData) {
          // ...
        });
      }

      注意到我們這里使用的是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ù)流嗎?好吧,理論上是可以的,讓我們嘗試一下。

      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)?

      Amazed

      是的。

      Promise++就是被觀察者(Observable),在Rx里你可以使用這樣的操作:var stream = Rx.Observable.fromPromise(promise),就可以很輕松的將Promise轉(zhuǎn)換成一個(gè)被觀察者(Observable),非常簡(jiǎn)單的操作就能讓我們現(xiàn)在就開始使用它。不同的是,這些被觀察者都不能兼容Promises/A+,但理論上并不沖突。一個(gè)Promise就是一個(gè)只有一個(gè)返回值的簡(jiǎn)單的被觀察者,而Rx就遠(yuǎn)超于Promise,它允許多個(gè)值返回。

      這樣更好,這樣更突出被觀察者至少比Promise強(qiáng)大,所以如果你相信Promise宣傳的東西,那么也請(qǐng)留意一下響應(yīng)式編程能勝任些什么。

      現(xiàn)在回到示例當(dāng)中,你應(yīng)該能快速發(fā)現(xiàn),我們?cè)?code>subscribe()方法的內(nèi)部再次調(diào)用了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)該這樣做試試。

      現(xiàn)在你需要了解的一個(gè)最基本的函數(shù)是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));
        });

      然后,我們創(chuàng)造了一個(gè)叫做"metastream"的怪獸:一個(gè)裝載了事件流的事件流。先別驚慌,metastream就是每一個(gè)發(fā)出的值都是另一個(gè)事件流的事件流,你看把它想象成一個(gè)[指針(pointers)]((https://en./wiki/Pointer_(computer_programming))數(shù)組:每一個(gè)單獨(dú)發(fā)出的值就是一個(gè)_指針_,它指向另一個(gè)事件流。在我們的示例里,每一個(gè)請(qǐng)求URL都映射到一個(gè)指向包含響應(yīng)數(shù)據(jù)的promise數(shù)據(jù)流。

      Response metastream

      一個(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ù):

      Response stream

      很贊,因?yàn)槲覀兊捻憫?yīng)事件流是根據(jù)請(qǐng)求事件流定義的,如果我們以后有更多事件發(fā)生在請(qǐng)求事件流的話,我們也將會(huì)在相應(yīng)的響應(yīng)事件流收到響應(yīng)事件,就如所期待的那樣:

      requestStream:  --a-----b--c------------|->
      responseStream: -----A--------B-----C---|->
      
      (小寫的是請(qǐng)求事件流, 大寫的是響應(yīng)事件流)
      

      現(xiàn)在,我們終于有響應(yīng)的事件流了,并且可以用我們收到的數(shù)據(jù)來(lái)渲染了:

      responseStream.subscribe(function(response) {
        // render `response` to the DOM however you wish
      });

      讓我們把所有代碼合起來(lái),看一下:

      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
      });

      刷新按鈕

      我還沒(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)聽者轉(zhuǎn)換成被觀察者的方法了。

      var refreshButton = document.querySelector('.refresh');
      var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');

      因?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。

      var requestStream = refreshClickStream
        .map(function() {
          var randomOffset = Math.floor(Math.random()*500);
          return 'https://api.github.com/users?since=' + randomOffset;
        });

      因?yàn)槲冶容^笨而且也沒(méi)有使用自動(dòng)化測(cè)試,所以我剛把之前做好的一個(gè)功能搞爛了。這樣,請(qǐng)求在一開始的時(shí)候就不會(huì)執(zhí)行,而只有在點(diǎn)擊事件發(fā)生時(shí)才會(huì)執(zhí)行。我們需要的是兩種情況都要執(zhí)行:剛開始打開網(wǎng)頁(yè)和點(diǎn)擊刷新按鈕都會(huì)執(zhí)行的請(qǐng)求。

      我們知道如何為每一種情況做一個(gè)單獨(dú)的事件流:

      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');

      但是我們是否可以將這兩個(gè)合并成一個(gè)呢?沒(méi)錯(cuò),是可以的,我們可以使用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----->
      

      現(xiàn)在做起來(lái)應(yīng)該很簡(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
      );

      還有一個(gè)更干凈的寫法,省去了中間事件流變量:

      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ù)做的事和你預(yù)期的完全一樣。無(wú)論你的輸入事件流是怎樣的,使用startWith(x)函數(shù)處理過(guò)后輸出的事件流一定是一個(gè)x 開頭的結(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;
        });

      不錯(cuò),如果你倒回到"搞爛了的自動(dòng)測(cè)試"的地方,然后再對(duì)比這兩個(gè)地方,你會(huì)發(fā)現(xiàn)我僅僅是加了一個(gè)startWith()函數(shù)而已。

      用事件流將3個(gè)推薦的用戶數(shù)據(jù)模型化

      直到現(xiàn)在,在響應(yīng)事件流(responseStream)的訂閱(subscribe())函數(shù)發(fā)生的渲染步驟里,我們只是稍微提及了一下推薦關(guān)注的UI?,F(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 
      });

      不,老兄,還沒(méi)那么快。我們又出現(xiàn)了新的問(wèn)題,因?yàn)槲覀儸F(xiàn)在有兩個(gè)訂閱者在影響著推薦關(guān)注的UI DOM元素(另一個(gè)是responseStream.subscribe()),這看起來(lái)并不符合關(guān)注分離(Separation of concerns)原則,還記得響應(yīng)式編程的原則么?

      Mantra

      現(xiàn)在,讓我們把推薦關(guān)注的用戶數(shù)據(jù)模型化成事件流形式,每個(gè)被發(fā)出的值是一個(gè)包含了推薦關(guān)注用戶數(shù)據(jù)的JSON對(duì)象。我們將把這三個(gè)用戶數(shù)據(jù)分開處理,下面是推薦關(guān)注的1號(hào)用戶數(shù)據(jù)的事件流:

      var suggestion1Stream = responseStream
        .map(function(listUsers) {
          // get one random user from the list
          return listUsers[Math.floor(Math.random()*listUsers.length)];
        });

      其他的,如推薦關(guān)注的2號(hào)用戶數(shù)據(jù)的事件流suggestion2Stream和推薦關(guān)注的3號(hào)用戶數(shù)據(jù)的事件流suggestion3Stream都可以方便的從suggestion1Stream 復(fù)制粘貼就好。這里并不是重復(fù)代碼,只是為讓我們的示例更加簡(jiǎn)單,而且我認(rèn)為這是一個(gè)思考如何避免重復(fù)代碼的好案例。

      Instead of having the rendering happen in responseStream's subscribe(), we do that here:

      suggestion1Stream.subscribe(function(suggestion) {
        // render the 1st suggestion to the DOM
      });

      我們不在responseStream的subscribe()中處理渲染了,我們這樣處理:

      suggestion1Stream.subscribe(function(suggestion) {
        // render the 1st suggestion to the DOM
      });

      回到"當(dāng)刷新時(shí),清楚掉當(dāng)前的推薦關(guān)注的用戶",我們可以很簡(jiǎn)單的把刷新點(diǎn)擊映射為沒(méi)有推薦數(shù)據(jù)(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; })
        );

      當(dāng)渲染時(shí),我們將 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
        }
      });

      現(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

      作為一種補(bǔ)充,我們可以在一開始的時(shí)候就渲染空的推薦內(nèi)容。這通過(guò)把startWith(null)添加到推薦關(guān)注的事件流就可以了:

      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);

      結(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ù)(responses)

      只剩這一個(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)求:

      var close1Button = document.querySelector('.close1');
      var close1ClickStream = Rx.Observable.fromEvent(close1Button, 'click');
      // and the same for close2Button and close3Button
      
      var 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;
        });

      這樣沒(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í)行responseStream函數(shù)來(lái)從響應(yīng)列表里隨機(jī)的抽出一個(gè)用戶數(shù)據(jù)來(lái),就像下面這樣:

          requestStream: --r--------------->
         responseStream: ------R----------->
      close1ClickStream: ------------c----->
      suggestion1Stream: ------s-----s----->
      

      在Rx中一個(gè)組合函數(shù)叫做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ā)出的值ab作為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)換成大寫的函數(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);

      現(xiàn)在,我們的拼圖還缺一小塊地方。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è)輸出值。

      這里有很多種方法來(lái)解決這個(gè)問(wèn)題,我們使用最簡(jiǎn)單的一種,也就是在啟動(dòng)的時(shí)候模擬'close 1'的點(diǎn)擊事件:

      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 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);
      // and the same logic for suggestion2Stream and suggestion3Stream
      
      suggestion1Stream.subscribe(function(suggestion) {
        if (suggestion === null) {
          // hide the first suggestion DOM element
        }
        else {
          // show the first suggestion DOM element
          // and render the data
        }
      });

      你可以在這里看到可演示的示例工程

      以上的代碼片段雖小但做到很多事:它適當(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ī)suggestion1Stream是'close 1'事件結(jié)合從最新的響應(yīng)數(shù)據(jù)中拿到的一個(gè)用戶數(shù)據(jù)的數(shù)據(jù)流,除此之外,當(dāng)刷新事件發(fā)生時(shí)和程序啟動(dòng)時(shí),它就是null。

      留意一下代碼中并未出現(xiàn)例如ifforwhile等流程控制語(yǔ)句,或者像JavaScript那樣典型的基于回調(diào)(callback-based)的流程控制。如果可以的話(稍候會(huì)給你留一些實(shí)現(xiàn)細(xì)節(jié)來(lái)作為練習(xí)),你甚至可以在subscribe()上使用 filter()函數(shù)來(lái)擺脫ifelse。在Rx里,我們有例如: mapfilterscanmergecombineLateststartWith等數(shù)據(jù)流的函數(shù),還有很多函數(shù)可以用來(lái)控制事件驅(qū)動(dòng)編程(event-driven program)的流程。這些函數(shù)的集合可以讓你使用更少的代碼實(shí)現(xiàn)更強(qiáng)大的功能。

      接下來(lái)

      如果你認(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)題,可以畫一下這些圖,思考一下,看一看這一大串函數(shù),然后繼續(xù)思考。以我個(gè)人經(jīng)驗(yàn),這樣效果很有效。

      一旦你開始使用了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ǔ)言的框架,它真的是你編寫任何事件驅(qū)動(dòng)程序,可以遵循的一個(gè)非常棒的編程范式。  

        本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
        轉(zhuǎn)藏 分享 獻(xiàn)花(0

        0條評(píng)論

        發(fā)表

        請(qǐng)遵守用戶 評(píng)論公約

        類似文章 更多