响应式编程,是明智的选择

相信大家在学习响应式编程这个新技术的时候都会充满了好奇,特别是它的一些变体,例如:Rx系列、Bacon.js、RAC等等……html

在缺少优秀资料的前提下,响应式编程的学习过程将尽是荆棘。起初,我试图寻找一些教程,却只找到少许的实践指南,并且它们讲的都很是浅显,历来没人接受围绕响应式编程创建一个完整知识体系的挑战。此外,官方文档一般也不能很好地帮助你理解某些函数,由于它们一般看起来很绕,不信请看这里:前端

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])java

根据元素下标,将可观察序列中每一个元素一一映射到一个新的可观察序列当中,而后...%…………%&¥#@@……&**(晕了)react

天呐,这简直太绕了!jquery

我读过两本相关的书,一本只是在给你描绘响应式编程的伟大景象,而另外一本却只是深刻到如何使用响应式库而已。我在不断的构建项目过程当中把响应式编程了解的透彻了一些,最后以这种艰难的方式学完了响应式编程。在我工做公司的一个实际项目中我会用到它,当我遇到问题时,还能够获得同事的支持。android

学习过程当中最难的部分是如何以响应式的方式来思考,更多的意味着要摒弃那些老旧的命令式和状态式的典型编程习惯,而且强迫本身的大脑以不一样的范式来运做。我尚未在网络上找到任何一个教程是从这个层面来剖析的,我以为这个世界很是值得拥有一个优秀的实践教程来教你如何以响应式编程的方式来思考,方便引导你开始学习响应式编程。而后看各类库文档才能够给你更多的指引。但愿这篇文章可以帮助你快速地进入响应式编程的世界。git

"什是响应式编程?"

网络上有一大堆糟糕的解释和定义,如Wikipedia上一般都是些很是笼统和理论性的解释,而Stackoverflow上的一些规范的回答显然也不适合新手来参考,Reactive Manifesto看起来也只像是拿给你的PM或者老板看的东西,微软的Rx术语"Rx = Observables + LINQ + Schedulers" 也显得太过沉重,并且充满了太多微软式的东西,反而给咱们带来更多疑惑。相对于你使用的MV*框架以及你钟爱的编程语言,"Reactive"和"Propagation of change"这样的术语并无传达任何有意义的概念。固然,个人view框架可以从model作出反应,个人改变固然也会传播,若是没有这些,个人界面根本就没有东西可渲染。github

因此,不要再扯这些废话了。web

响应式编程就是与异步数据流交互的编程范式

一方面,这已经不是什么新事物了。事件总线(Event Buses)或一些典型的点击事件本质上就是一个异步事件流(asynchronous event stream),这样你就能够观察它的变化并使其作出一些反应(do some side effects)。响应式是这样的一个思路:除了点击和悬停(hover)的事件,你还能够给其余任何事物建立数据流。数据流无处不在,任何东西均可以成为一个数据流,例如变量、用户输入、属性、缓存、数据结构等等。举个栗子,你能够把你的微博订阅功能想象成跟点击事件同样的数据流,你能够监听这样的数据流,并作出相应的反应。ajax

最重要的是,你会拥有一些使人惊艳的函数去结合、建立和过滤任何一组数据流。 这就是"函数式编程"的魔力所在。一个数据流能够做为另外一个数据流的输入,甚至多个数据流也能够做为另外一个数据流的输入。你能够合并两个数据流,也能够过滤一个数据流获得另外一个只包含你感兴趣的事件的数据流,还能够映射一个数据流的值到一个新的数据流里。

数据流是整个响应式编程体系中的核心,要想学习响应式编程,固然要先走进数据流一探究竟了。那如今就让咱们先从熟悉的"点击一个按钮"的事件流开始

Click event stream

一个数据流是一个按时间排序的即将发生的事件(Ongoing events ordered in time)的序列。如上图,它能够发出3种不一样的事件(上一句已经把它们叫作事件):一个某种类型的值事件,一个错误事件和一个完成事件。当一个完成事件发生时,在某些状况下,咱们可能会作这样的操做:关闭包含那个按钮的窗口或者视图组件。

咱们只能异步捕捉被发出的事件,使得咱们能够在发出一个值事件时执行一个函数,发出错误事件时执行一个函数,发出完成事件时执行另外一个函数。有时候你能够忽略后两个事件,只需聚焦于如何定义和设计在发出值事件时要执行的函数,监听这个事件流的过程叫作订阅,咱们定义的函数叫作观察者,而事件流就能够叫作被观察的主题(或者叫被观察者)。你应该察觉到了,对的,它就是观察者模式

上面的示意图咱们也能够用ASCII码的形式从新画一遍,请注意,下面的部分教程中咱们会继续使用这幅图:

--a---b-c---d---X---|->

a, b, c, d 是值事件
X 是错误事件
| 是完成事件
---> 是时间线(轴)

如今你对响应式编程事件流应该很是熟悉了,为了避免让你感到无聊,让咱们来作一些新的尝试吧:咱们将建立一个由原始点击事件流演变而来的一种新的点击事件流。

首先,让咱们来建立一个记录按钮点击次数的事件流。在经常使用的响应式库中,每一个事件流都会附有一些函数,例如 map,filterscan等,当你调用这其中的一个方法时,好比clickStream.map(f),它会返回基于点击事件流的一个新事件流。它不会对原来的点击事件流作任何的修改。这种特性叫作不可变性(immutability),并且它能够和响应式事件流搭配在一块儿使用,就像豆浆和油条同样完美的搭配。这样咱们能够用链式函数的方式来调用,例如: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)函数会根据你提供的f函数把原事件流中每个返回值分别映射到新的事件流中。在上图的例子中,咱们把每一次点击事件都映射成数字1,scan(g)函数则把以前映射的值汇集起来,而后根据x = g(accumulated, current)算法来做相应的处理,而本例的g函数其实就是简单的加法函数。而后,当一个点击事件发生时,counterStream函数则上报当前点击事件总数。

为了展现响应式编程真正的魅力,咱们假设你有一个"双击"事件流,为了让它更有趣,咱们假设这个事件流同时处理"三次点击"或者"屡次点击"事件,而后深吸一口气想一想如何用传统的命令式和状态式的方式来处理,我敢打赌,这么作会至关的讨厌,其中还要涉及到一些变量来保存状态,而且还得作一些时间间隔的调整。

而用响应式编程的方式处理会很是的简洁,实际上,逻辑处理部分只须要四行代码。可是,当前阶段让咱们现忽略代码的部分,不管你是新手仍是专家,看着图表思考来理解和创建事件流将是一个很是棒的方法。

屡次点击事件流

图中,灰色盒子表示将上面的事件流转换下面的事件流的函数过程,首先根据250毫秒的间隔时间(event silence, 译者注:无事件发生的时间段,上一个事件发生到下一个事件发生的间隔时间)把点击事件流一段一隔开,再将每一段的一个或多个点击事件添加到列表中(这就是这个函数:buffer(stream.throttle(250ms))所作的事情,当前咱们先不要急着去理解细节,咱们只需专一响应式的部分先)。如今咱们获得的是多个含有事件流的列表,而后咱们使用了map()中的函数来算出每个列表长度的整数数值映射到下一个事件流当中。最后咱们使用了过滤filter(x >= 2) 函数忽略掉了小于1 的整数。就这样,咱们用了3步操做生成了咱们想要的事件流,接下来,咱们就能够订阅("监听")这个事件并做出咱们想要的操做了。

我但愿你能感觉到这个示例的优雅之处。固然了,这个示例也只是响应式编程魔力的冰山一角而已,你一样能够将这3步操做应用到不一样种类的事件流中去,例如,一串API响应的事件流。另外一方面,你还有很是多的函数可使用。

"我为何要采用响应式编程?"

响应式编程能够加深你代码抽象的程度,让你能够更专一于定义与事件相互依赖的业务逻辑,而不是把大量精力放在实现细节上,同时,使用响应式编程还能让你的代码变得更加简洁。

特别对于如今流行的webapps和mobile apps,它们的 UI 事件与数据频繁地产生交互,在开发这些应用时使用响应式编程的优势将更加明显。十年前,web页面的交互是经过提交一个很长的表单数据到后端,而后再作一些简单的前端渲染操做。而如今的Apps则演变的更具备实时性:仅仅修改一个单独的表单域就能自动的触发保存到后端的代码,就像某个用户对一些内容点了赞,就可以实时反映到其余已链接的用户同样,等等。

当今的Apps都含有丰富的实时事件来保证一个高效的用户体验,咱们就须要采用一个合适的工具来处理,那么响应式编程就正好是咱们想要的答案。

以响应式编程方式思考的例子

让咱们深刻到一些真实的例子,一个可以一步一步教你如何以响应式编程的方式思考的例子,没有虚构的示例,没有只知其一;不知其二的概念。在这个教程的末尾咱们将产生一些真实的函数代码,并可以知晓每一步为何那样作的缘由(知其然,知其因此然)。

我选了JavaScript和RxJS来做为本教程的编程语言,缘由是:JavaScript是目前最多人熟悉的语言,而Rx系列的库对于不少语言和平台的运用是很是普遍的,例如(.NETJavaScalaClojureJavaScriptRubyPythonC++Objective-C/Cocoa,Groovy等等。因此,不管你用的是什么语言、库、工具,你都能从下面这个教程中学到东西(从中受益)。

实现一个推荐关注(Who to follow)的功能

在Twitter里有一个UI元素向你推荐你能够关注的用户,以下图:

Twitter Who to follow suggestions box

咱们将聚焦于模仿它的主要功能,它们是:

  • 开始阶段,从API加载推荐关注的用户帐户数据,而后显示三个推荐用户
  • 点击刷新,加载另外三个推荐用户到当前的三行中显示
  • 点击每一行的推荐用户上的'x'按钮,清楚当前被点击的用户,并显示新的一个用户到当前行
  • 每一行显示一个用户的头像而且在点击以后能够连接到他们的主页。

咱们能够先无论其余的功能和按钮,由于它们是次要的。由于Twitter最近关闭了未经受权的公共API调用,咱们将用Github获取用户的API代替,而且以此来构建咱们的UI。

若是你想先看一下最终效果,这里有完成后的代码

Request和Response

在Rx中是怎么处理这个问题呢?,在开始以前,咱们要明白,(几乎)一切均可以成为一个事件流,这就是Rx的准则(mantra)。让咱们从最简单的功能开始:"开始阶段,从API加载推荐关注的用户帐户数据,而后显示三个推荐用户"。其实这个功能没什么特殊的,简单的步骤分为: (1)发出一个请求,(2)获取响应数据,(3)渲染响应数据。ok,让咱们把请求做为一个事件流,一开始你可能会以为这样作有些夸张,但别急,咱们也得从最基本的开始,不是吗?

开始时咱们只需作一次请求,若是咱们把它做为一个数据流的话,它只能成为一个仅仅返回一个值的事件流而已。一下子咱们还会有不少请求要作,但当前,只有一个。

--a------|->

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

这是一个咱们要请求的URL事件流。每当发生一个请求时,它将告诉咱们两件事:何时和作了什么事(when and what)。何时请求被执行,何时事件就被发出。而作了什么就是请求了什么,也就是请求的URL字符串。

在Rx中,建立返回一个值的事件流是很是简单的。其实事件流在Rx里的术语是叫"被观察者",也就是说它是能够被观察的,可是我发现这名字比较傻,因此我更喜欢把它叫作事件流

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

但如今,这只是一个字符串的事件流而已,并无作其余操做,因此咱们须要在发出这个值的时候作一些咱们要作的操做,能够经过订阅(subscribing)这个事件来实现。

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

注意到咱们这里使用的是JQuery的AJAX回调方法(咱们假设你已经很了解JQuery和AJAX了)来的处理这个异步的请求操做。可是,请稍等一下,Rx就是用来处理异步数据流的,难道它就不能处理来自请求(request)在将来某个时间响应(response)的数据流吗?好吧,理论上是能够的,让咱们尝试一下。

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()操做就是在建立本身定制的事件流,且对于数据事件(onNext())和错误事件(onError())都会显示的通知该事件每个观察者(或订阅者)。咱们作的只是小小的封装一下jQuery Ajax Promise而已。等等,这是否意味者jQuery Ajax Promise本质上就是一个被观察者呢(Observable)?

Amazed

是的。

Promise++就是被观察者(Observable),在Rx里你可使用这样的操做:var stream = Rx.Observable.fromPromise(promise),就能够很轻松的将Promise转换成一个被观察者(Observable),很是简单的操做就能让咱们如今就开始使用它。不一样的是,这些被观察者都不能兼容Promises/A+,但理论上并不冲突。一个Promise就是一个只有一个返回值的简单的被观察者,而Rx就远超于Promise,它容许多个值返回。

这样更好,这样更突出被观察者至少比Promise强大,因此若是你相信Promise宣传的东西,那么也请留意一下响应式编程能胜任些什么。

如今回到示例当中,你应该能快速发现,咱们在subscribe()方法的内部再次调用了subscribe()方法,这有点相似于回调地狱(callback hell),并且responseStream的建立也是依赖于requestStream的。在以前咱们说过,在Rx里,有不少很简单的机制来从其余事件流的转化并建立出一些新的事件流,那么,咱们也应该这样作试试。

如今你须要了解的一个最基本的函数是map(f),它能够从事件流A中取出每个值,并对每个值执行f()函数,而后将产生的新值填充到事件流B。若是将它应用到咱们的请求和响应事件流当中,那咱们就能够将请求的URL映射到一个响应Promises上了(假装成数据流)。

var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });

而后,咱们创造了一个叫作"metastream"的怪兽:一个装载了事件流的事件流。先别惊慌,metastream就是每个发出的值都是另外一个事件流的事件流,你看把它想象成一个[指针(pointers)]((https://en.wikipedia.org/wiki/Pointer_(computer_programming))数组:每个单独发出的值就是一个_指针_,它指向另外一个事件流。在咱们的示例里,每个请求URL都映射到一个指向包含响应数据的promise数据流。

Response metastream

一个响应的metastream,看起来确实让人容易困惑,看样子对咱们一点帮助也没有。咱们只想要一个简单的响应数据流,每个发出的值是一个简单的JSON对象就行,而不是一个'Promise' 的JSON对象。ok,让咱们来见识一下另外一个函数:Flatmap,它是map()函数的另外一个版本,它比metastream更扁平。一切在"主躯干"事件流发出的事件都将在"分支"事件流中发出。Flatmap并非metastreams的修复版,metastreams也不是一个bug。它俩在Rx中都是处理异步响应事件的好工具、好帮手。

var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });

Response stream

很赞,由于咱们的响应事件流是根据请求事件流定义的,若是咱们之后有更多事件发生在请求事件流的话,咱们也将会在相应的响应事件流收到响应事件,就如所期待的那样:

requestStream:  --a-----b--c------------|->
responseStream: -----A--------B-----C---|->

(小写的是请求事件流, 大写的是响应事件流)

如今,咱们终于有响应的事件流了,而且能够用咱们收到的数据来渲染了:

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

刷新按钮

我还没提到本次响应的JSON数据是含有100个用户数据的list,这个API只容许指定页面偏移量(page offset),而不能指定每页大小(page size),咱们只用到了3个用户数据而浪费了其余97个,如今能够先忽略这个问题,稍后咱们将学习如何缓存响应的数据。

每当刷新按钮被点击,请求事件流就会发出一个新的URL值,这样咱们就能够获取新的响应数据。这里咱们须要两个东西:点击刷新按钮的事件流(准则:一切都能做为事件流),咱们须要将点击刷新按钮的事件流做为请求事件流的依赖(即点击刷新事件流会引发请求事件流)。幸运的是,RxJS已经有了能够从事件监听者转换成被观察者的方法了。

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

由于刷新按钮点击事件不会携带将要请求的API的URL,咱们须要将每次的点击映射到一个实际的URL上,如今咱们将请求事件流转换成了一个点击事件流,并将每次的点击映射成一个随机的页面偏移量(offset)参数来组成API的URL。

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()方法来实现。下图能够解释merge()函数的用处:

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()函数作的事和你预期的彻底同样。不管你的输入事件流是怎样的,使用startWith(x)函数处理事后输出的事件流必定是一个x 开头的结果。可是我没有老是重复代码( DRY),我只是在重复API的URL字符串,改进的方法是将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; });

不错,若是你倒回到"搞烂了的自动测试"的地方,而后再对比这两个地方,你会发现我仅仅是加了一个startWith()函数而已。

用事件流将3个推荐的用户数据模型化

直到如今,在响应事件流(responseStream)的订阅(subscribe())函数发生的渲染步骤里,咱们只是稍微说起了一下推荐关注的UI。如今有了刷新按钮,咱们就会出现一个问题:当你点击了刷新按钮,当前的三个推荐关注用户没有被清楚,而只要响应的数据达到后咱们就拿到了新的推荐关注的用户数据,为了让UI看起来更漂亮,咱们须要在点击刷新按钮的事件发生的时候清楚当前的三个推荐关注的用户。

refreshClickStream.subscribe(function() { // clear the 3 suggestion DOM elements });

不,老兄,还没那么快。咱们又出现了新的问题,由于咱们如今有两个订阅者在影响着推荐关注的UI DOM元素(另外一个是responseStream.subscribe()),这看起来并不符合关注分离(Separation of concerns)原则,还记得响应式编程的原则么?

Mantra

如今,让咱们把推荐关注的用户数据模型化成事件流形式,每一个被发出的值是一个包含了推荐关注用户数据的JSON对象。咱们将把这三个用户数据分开处理,下面是推荐关注的1号用户数据的事件流:

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

其余的,如推荐关注的2号用户数据的事件流suggestion2Stream和推荐关注的3号用户数据的事件流suggestion3Stream均可以方便的从suggestion1Stream 复制粘贴就好。这里并非重复代码,只是为让咱们的示例更加简单,并且我认为这是一个思考如何避免重复代码的好案例。

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

回到"当刷新时,清楚掉当前的推荐关注的用户",咱们能够很简单的把刷新点击映射为没有推荐数据(null suggestion data),而且在suggestion1Stream中包含进来,以下:

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解释为"没有数据",而后把UI元素隐藏起来。

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

做为一种补充,咱们能够在一开始的时候就渲染空的推荐内容。这经过把startWith(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-->

推荐关注的关闭和使用已缓存的响应数据(responses)

只剩这一个功能没有实现了,每一个推荐关注的用户UI会有一个'x'按钮来关闭本身,而后在当前的用户数据UI中加载另外一个推荐关注的用户。最初的想法是:点击任何关闭按钮时都须要发起一个新的请求:

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

这样没什么效果,这样会关闭和从新加载所有的推荐关注用户,而不只仅是处理咱们点击的那一个。这里有几种方式来解决这个问题,而且让它变得有趣,咱们将重用以前的请求数据来解决这个问题。这个API响应的每页数据大小是100个用户数据,而咱们只使用了其中三个,因此还有一大堆未使用的数据能够拿来用,不用去请求更多数据了。

ok,再来,咱们继续用事件流的方式来思考。当'close1'点击事件发生时,咱们想要使用最近发出的响应数据,并执行responseStream函数来从响应列表里随机的抽出一个用户数据来,就像下面这样:

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

在Rx中一个组合函数叫作combineLatest,应该是咱们须要的。这个函数会把数据流A和数据流B做为输入,而且不管哪个数据流发出一个值了,combineLatest 函数就会将从两个数据流最近发出的值ab做为f函数的输入,计算后返回一个输出值(c = f(x,y)),下面的图表会让这个函数的过程看起来会更加清晰:

stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
          vvvvvvvv combineLatest(f) vvvvvvv
          ----AB---AC--EC---ED--ID--IQ---->

f是转换成大写的函数

这样,咱们就能够把combineLatest()函数用在close1ClickStream和 responseStream上了,只要关闭按钮被点击,咱们就能够得到最近的响应数据,并在suggestion1Stream上产生出一个新值。另外一方面,combineLatest()函数也是相对的:每当在responseStream上发出一个新的响应,它将会结合一次新的点击关闭按钮事件来产生一个新的推荐关注的用户数据,这很是有趣,由于它能够给咱们的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);

如今,咱们的拼图还缺一小块地方。combineLatest()函数使用了最近的两个数据源,可是若是某一个数据源尚未发出任何东西,combineLatest()函数就不能在输出流上产生一个数据事件。若是你看了上面的ASCII图表(文章中第一个图表),你会明白当第一个数据流发出一个值a时并无任何的输出,只有当第二个数据流发出一个值b的时候才会产生一个输出值。

这里有不少种方法来解决这个问题,咱们使用最简单的一种,也就是在启动的时候模拟'close 1'的点击事件:

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

封装起来

咱们完成了,下面是封装好的完整示例代码:

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

你能够在这里看到可演示的示例工程

以上的代码片断虽小但作到不少事:它适当的使用关注分离(separation of concerns)原则的实现了对多个事件流的管理,甚至作到了响应数据的缓存。这种函数式的风格使得代码看起来更像是声明式编程而非命令式编程:咱们并非在给一组指令去执行,只是定义了事件流之间关系来告诉它这是什么。例如,咱们用Rx来告诉计算机suggestion1Stream是'close 1'事件结合从最新的响应数据中拿到的一个用户数据的数据流,除此以外,当刷新事件发生时和程序启动时,它就是null

留意一下代码中并未出现例如ifforwhile等流程控制语句,或者像JavaScript那样典型的基于回调(callback-based)的流程控制。若是能够的话(稍候会给你留一些实现细节来做为练习),你甚至能够在subscribe()上使用 filter()函数来摆脱ifelse。在Rx里,咱们有例如: mapfilterscanmergecombineLateststartWith等数据流的函数,还有不少函数能够用来控制事件驱动编程(event-driven program)的流程。这些函数的集合可让你使用更少的代码实现更强大的功能。

接下来

若是你认为Rx将会成为你首选的响应式编程库,接下来就须要花一些时间来熟悉一大批的函数用来变形、联合和建立被观察者。若是你想在事件流的图表当中熟悉这些函数,那就来看一下这个:RxJava's very useful documentation with marble diagrams。请记住,不管什么时候你遇到问题,能够画一下这些图,思考一下,看一看这一大串函数,而后继续思考。以我我的经验,这样效果颇有效。

一旦你开始使用了Rx编程,请记住,理解Cold vs Hot Observables的概念是很是必要的,若是你忽视了这一点,它就会反弹回来并残忍的反咬你一口。我这里已经警告你了,学习函数式编程能够提升你的技能,熟悉一些常见问题,例如Rx会带来的反作用

可是响应式编程库并不只仅是Rx,还有相对容易理解的,没有Rx那些怪癖的Bacon.jsElm Language则以它本身的方式支持响应式编程:它是一门会编译成Javascript + HTML + CSS的响应式编程语言,并有一个time travelling debugger功能,很棒吧。

而Rx对于像前端和App这样须要处理大量的编程效果是很是棒的。可是它不仅是能够用在客户端,还能够用在后端或者接近数据库的地方。事实上,RxJava就是Netflix服务端API用来处理并行的组件。Rx并非局限于某种应用程序或者编程语言的框架,它真的是你编写任何事件驱动程序,能够遵循的一个很是棒的编程范式。

相关文章
相关标签/搜索