So you're curious in learning this new thing called Reactive Programming, particularly its variant comprising of Rx, Bacon.js, RAC, and others.html
相信大家在学习响应式编程这个新技术的时候都会充满了好奇,特别是它的一些变体,包括Rx系列、Bacon.js、RAC和其余的一些变体。前端
Learning it is hard, even harder by the lack of good material. When I started, I tried looking for tutorials. I found only a handful of practical guides, but they just scratched the surface and never tackled the challenge of building the whole architecture around it. Library documentations often don't help when you're trying to understand some function. I mean, honestly, look at this:java
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])react
Projects each element of an observable sequence into a new sequence of observable sequences by incorporating the element's index and then transforms an observable sequence of observable sequences into an observable sequence producing values only from the most recent observable sequence.jquery
Holy cow.android
学习响应式编程是个困难的过程,尤为是在当前缺少优秀资料的前提下。起初,我试图寻找一些教程,却只找到了少许的实践指南,并且它们讲的都很是浅显(scratched the surface),历来没人接受围绕着响应式编程创建一个完整知识体系的挑战。而官方文档一般也并不能彻底地帮助你理解某些函数,它们一般看起来很绕,不信请看这里:git
Rx.Observable.prototype.flatMapLatest(selector, [thisArg])github
根据元素下标,将可观察序列中每一个元素一一映射到一个新的可观察序列当中,而后...%…………%&¥#@@……&**(晕了)web
天呐,这简直太绕了!ajax
I've read two books, one just painted the big picture, while the other dived into how to use the Reactive library. I ended up learning Reactive Programming the hard way: figuring it out while building with it. At my work in Futurice I got to use it in a real project, and had the support of some colleagues when I ran into troubles.
The hardest part of the learning journey is thinking in Reactive. It's a lot about letting go of old imperative and stateful habits of typical programming, and forcing your brain to work in a different paradigm. I haven't found any guide on the internet in this aspect, and I think the world deserves a practical tutorial on how to think in Reactive, so that you can get started. Library documentation can light your way after that. I hope this helps you.
我读过两本相关的书,一本只是在给你描绘响应式编程的伟大景象,而另外一本却只是深刻到如何使用响应式库而已。我在不断的构建(building)中把响应式编程了解的透彻了一些,最后以这种艰难的方式学完了响应式编程。在我工做公司的一个真实项目中我会用到它,当我遇到问题时,还能够获得同事的支持。
学习过程当中最难的部分是如何以响应式的方式来思考,更多的意味着要摒弃那些老旧的命令式和状态式的典型编程习惯,而且强迫本身的大脑以不一样的范式来运做。我尚未在网络上找到任何一个教程是从这个层面来剖析的,我以为这个世界很是值得拥有一个优秀的实践教程来教你如何以响应式编程的方式来思考,方便引导你开始学习响应式编程。而后看各类库文档才能够给你更多的指引。但愿这篇文章可以帮助你快速地进入响应式编程的世界。
There are plenty of bad explanations and definitions out there on the internet. Wikipedia is too generic and theoretical as usual.Stackoverflow's canonical answer is obviously not suitable for newcomers. Reactive Manifesto sounds like the kind of thing you show to your project manager or the businessmen at your company. Microsoft's Rx terminology "Rx = Observables + LINQ + Schedulers" is so heavy and Microsoftish that most of us are left confused. Terms like "reactive" and "propagation of change" don't convey anything specifically different to what your typical MV* and favorite language already does. Of course my framework views react to the models. Of course change is propagated. If it wouldn't, nothing would be rendered.
So let's cut the bullshit.
网络上有一大堆糟糕的解释和定义,Wikipedia上一般都是些很是笼统和理论性的解释,Stackoverflow上的一些规范的回答显然也不适合新手来参考,Reactive Manifesto看起来也只像是拿给你的PM或者老板看的东西,微软的Rx术语"Rx = Observables + LINQ + Schedulers" 也显得太过沉重,并且充满了太多微软式的东西,反而给咱们留下了更多的疑惑。相对于你使用的MV*框架以及你钟爱的编程语言,"Reactive"和"Propagation of change"这样的术语并无传达任何有意义的概念。固然,个人view框架可以从model作出反应,个人改变固然也会传播,若是没有这些,个人界面根本就没有东西可渲染(译者:最后一句旨在说明微软的术语化的东西都很是老套、很是教科书化)。
因此,不要再扯这些废话了。
In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. Reactive is that idea on steroids. You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc. For example, imagine your Twitter feed would be a data stream in the same fashion that click events are. You can listen to that stream and react accordingly.
一方面,这已经不是什么新事物了。事件总线(Event Buses)或一些典型的点击事件本质上就是一个异步事件流(asynchronous event stream),这样你就能够观察它的变化并使其作出一些反应(产生一些效果(do some side effects))。响应式是这样的一个思路:除了点击(click)和悬停(hover)的事件外,你能够给任何事物建立数据流。数据流无处不在(Streams are cheap and ubiquitous),任何东西均可以成为一个数据流,例如变量、用户输入、属性、缓存、数据结构等等。举个栗子,你能够把你的微博订阅功能想象成跟点击事件同样的数据流,你能够监听这样的数据流,并作出相应的反应。
On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams.That's where the "functional" magic kicks in. A stream can be used as an input to another one. Even multiple streams can be used as inputs to another stream. You can merge two streams. You can filter a stream to get another one that has only those events you are interested in. You can map data values from one stream to another new one.
If streams are so central to Reactive, let's take a careful look at them, starting with our familiar "clicks on a button" event stream.
A stream is a sequence of ongoing events ordered in time. It can emit three different things: a value (of some type), an error, or a "completed" signal. Consider that the "completed" takes place, for instance, when the current window or view containing that button is closed.
We capture these emitted events only asynchronously, by defining a function that will execute when a value is emitted, another function when an error is emitted, and another function when 'completed' is emitted. Sometimes these last two can be omitted and you can just focus on defining the function for values. The "listening" to the stream is called subscribing. The functions we are defining are observers. The stream is the subject (or "observable") being observed. This is precisely the Observer Design Pattern.
An alternative way of drawing that diagram is with ASCII, which we will use in some parts of this tutorial:
--a---b-c---d---X---|-> a, b, c, d are emitted values X is an error | is the 'completed' signal ---> is the timeline
Since this feels so familiar already, and I don't want you to get bored, let's do something new: we are going to create new click event streams transformed out of the original click event stream.
最重要的是,你会拥有一些使人惊艳的函数去结合(combine)、建立(create)和过滤(filter)任何一组数据流。 这就是"函数式编程"的魔力所在。一个数据流能够做为另外一个数据流的输入,甚至多个数据流也能够做为另外一个数据流的输入。你能够合并(merge)两个数据流,也能够过滤(filter)一个数据流获得另外一个只包含你感兴趣的事件的数据流,还能够映射(map)一个数据流的值到一个新的数据流里。
若是数据流对于响应式是如此的核心(so central to Reactive),那就让咱们来仔细的看看它们,先从咱们熟悉的"点击一个按钮"的事件流开始
一个数据流是一个按时间排序的即将发生的事件(Ongoing events ordered in time)的序列。如上图,它能够发出3种不一样的事件(上一句已经把它们叫作事件):一个某种类型的值事件,一个错误事件和一个完成事件。当一个完成事件发生时,在某些状况下,咱们可能会作这样的操做:关闭包含那个按钮的窗口或者视图组件。
咱们只能异步的的去捕捉这些被发出的事件,这样咱们就能够在发出一个值事件时执行一个函数,发出错误事件时执行一个函数,发出完成事件时执行另外一个函数。有时候你能够忽略后两个事件,只需聚焦于如何定义和设计在发出值事件时要执行的函数,监听这个事件流的过程叫作订阅,咱们定义的函数叫作观察者,而事件流就能够叫作被观察的主题(或者叫被观察者)。你应该察觉到了,对的,它就是观察者模式。
上面的示意图咱们也能够用ASCII码的形式从新画一遍,请注意,下面的部分教程中咱们会继续使用这幅图:
--a---b-c---d---X---|-> a, b, c, d 是值事件 X 是错误事件 | 是完成事件 ---> 是时间线(轴)
如今你对响应式编程事件流应该很是熟悉了,为了避免让你感到无聊,让咱们来作一些新的尝试吧:咱们将建立一个由原始点击事件流演变而来的一种新的点击事件流。
First, let's make a counter stream that indicates how many times a button was clicked. In common Reactive libraries, each stream has many functions attached to it, such as map
, filter
, scan
, etc. When you call one of these functions, such asclickStream.map(f)
, it returns a new stream based on the click stream. It does not modify the original click stream in any way. This is a property called immutability, and it goes together with Reactive streams just like pancakes are good with syrup. That allows us to chain functions like 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-->
The map(f)
function replaces (into the new stream) each emitted value according to a function f
you provide. In our case, we mapped to the number 1 on each click. The scan(g)
function aggregates all previous values on the stream, producing value x = g(accumulated, current)
, where g
was simply the add function in this example. Then, counterStream
emits the total number of clicks whenever a click happens.
首先,让咱们来建立一个记录按钮点击次数的事件流。在经常使用的响应式库中,每一个事件流都会附有一些函数,例如 map
,filter
, scan
等,当你调用这其中的一个方法时,好比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
函数则上报当前点击事件总数。
To show the real power of Reactive, let's just say that you want to have a stream of "double click" events. To make it even more interesting, let's say we want the new stream to consider triple clicks as double clicks, or in general, multiple clicks (two or more). Take a deep breath and imagine how you would do that in a traditional imperative and stateful fashion. I bet it sounds fairly nasty and involves some variables to keep state and some fiddling with time intervals.
Well, in Reactive it's pretty simple. In fact, the logic is just 4 lines of code. But let's ignore code for now. Thinking in diagrams is the best way to understand and build streams, whether you're a beginner or an expert.
Grey boxes are functions transforming one stream into another. First we accumulate clicks in lists, whenever 250 milliseconds of "event silence" has happened (that's what buffer(stream.throttle(250ms))
does, in a nutshell. Don't worry about understanding the details at this point, we are just demoing Reactive for now). The result is a stream of lists, from which we applymap()
to map each list to an integer matching the length of that list. Finally, we ignore 1
integers using the filter(x >= 2)
function. That's it: 3 operations to produce our intended stream. We can then subscribe ("listen") to it to react accordingly how we wish.
I hope you enjoy the beauty of this approach. This example is just the tip of the iceberg: you can apply the same operations on different kinds of streams, for instance, on a stream of API responses; on the other hand, there are many other functions available.
为了展现响应式编程真正的魅力,咱们假设你有一个"双击"事件流,为了让它更有趣,咱们假设这个事件流同时处理"三次点击"或者"屡次点击"事件,而后深吸一口气想一想如何用传统的命令式和状态式的方式来处理,我敢打赌,这么作会至关的讨厌,其中还要涉及到一些变量来保存状态,而且还得作一些时间间隔的调整。
而用响应式编程的方式处理会很是的简洁,实际上,逻辑处理部分只须要四行代码。可是,当前阶段让咱们现忽略代码的部分,不管你是新手仍是专家,看着图表思考来理解和创建事件流将是一个很是棒的方法。
图中,灰色盒子表示将上面的事件流转换下面的事件流的函数过程,首先根据250毫秒的间隔时间(event silence, 译者:无事件发生的时间段,上一个事件发生到下一个事件发生的间隔时间)把点击事件流一段一隔开,再将每一段的一个或多个点击事件添加到列表中(这就是这个函数:buffer(stream.throttle(250ms))
所作的事情,当前咱们先不要急着去理解细节,咱们只需专一响应式的部分先)。如今咱们获得的是多个含有事件流的列表,而后咱们使用了map()
中的函数来算出每个列表长度的整数数值映射到下一个事件流当中。最后咱们使用了过滤filter(x >= 2)
函数忽略掉了小于1
的整数。就这样,咱们用了3步操做生成了咱们想要的事件流,接下来,咱们就能够订阅("监听")这个事件并做出咱们想要的操做了。
我但愿你能感觉到这个示例的优雅之处。固然了,这个示例也只是响应式编程魔力的冰山一角而已,你一样能够将这3步操做应用到不一样种类的事件流中去,例如,一串API响应的事件流。另外一方面,你还有很是多的函数可使用。
Reactive Programming raises the level of abstraction of your code so you can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details. Code in RP will likely be more concise.
The benefit is more evident in modern webapps and mobile apps that are highly interactive with a multitude of UI events related to data events. 10 years ago, interaction with web pages was basically about submitting a long form to the backend and performing simple rendering to the frontend. Apps have evolved to be more real-time: modifying a single form field can automatically trigger a save to the backend, "likes" to some content can be reflected in real time to other connected users, and so forth.
Apps nowadays have an abundancy of real-time events of every kind that enable a highly interactive experience to the user. We need tools for properly dealing with that, and Reactive Programming is an answer.
响应式编程能够提升你的代码抽象级别,好让你能够专一于定义与事件相互依存的业务逻辑,而不是把大量精力放在实现细节上,使用响应式编程会让你的代码变得更加简洁。
特别对于如今流行的webapps和mobile apps,这些频繁与数据事件相关的大量UI事件交互的程序,好处就更加的明显了。十年前,web页面的交互是经过提交一个很长的表单数据到后端,而后再作一些简单的前端渲染操做。而如今的Apps则演变的更具备实时性:仅仅修改一个单独的表单域就能自动的触发保存到后端的代码,就像某个用户对一些内容点了赞,就可以实时反映到其余已链接的用户同样,等等。
当今的Apps都含有丰富的实时事件来保证一个高效的用户体验,咱们就须要采用一个合适的工具来处理,那么响应式编程就正好是咱们想要的答案。
Let's dive into the real stuff. A real-world example with a step-by-step guide on how to think in RP. No synthetic examples, no half-explained concepts. By the end of this tutorial we will have produced real functioning code, while knowing why we did each thing.
I picked JavaScript and RxJS as the tools for this, for a reason: JavaScript is the most familiar language out there at the moment, and the Rx* library family is widely available for many languages and platforms (.NET, Java, Scala, Clojure, JavaScript,Ruby, Python, C++, Objective-C/Cocoa, Groovy, etc). So whatever your tools are, you can concretely benefit by following this tutorial.
让咱们深刻到一些真实的例子,一个可以一步一步教你如何以响应式编程的方式思考的例子,没有虚构的示例,没有只知其一;不知其二的概念。在这个教程的末尾咱们将产生一些真实的函数代码,并可以知晓每一步为何那样作的缘由(知其然,知其因此然)。
我选了JavaScript和RxJS来做为本教程的编程语言,缘由是:JavaScript是目前最多人熟悉的语言,而Rx系列的库对于不少语言和平台的运用是很是普遍的,例如(.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy等等。因此,不管你用的是什么语言、库、工具,你都能从下面这个教程中学到东西(从中受益)。
In Twitter there is this UI element that suggests other accounts you could follow:
We are going to focus on imitating its core features, which are:
We can leave out the other features and buttons because they are minor. And, instead of Twitter, which recently closed its API to the unauthorized public, let's build that UI for following people on Github. There's a Github API for getting users.
The complete code for this is ready at http://jsfiddle.net/staltz/8jFJH/48/ in case you want to take a peak already.
在Twitter里有一个UI元素向你推荐你能够关注的用户,以下图:
咱们将聚焦于模仿它的主要功能,它们是:
咱们能够先无论其余的功能和按钮,由于它们是次要的。由于Twitter最近关闭了未经受权的公共API调用,咱们将用Github获取用户的API代替,而且以此来构建咱们的UI。
若是你想先看一下最终效果,这里有完成后的代码。
How do you approach this problem with Rx? Well, to start with, (almost) everything can be a stream. That's the Rx mantra. Let's start with the easiest feature: "on startup, load 3 accounts data from the API". There is nothing special here, this is simply about (1) doing a request, (2) getting a response, (3) rendering the response. So let's go ahead and represent our requests as a stream. At first this will feel like overkill, but we need to start from the basics, right?
On startup we need to do only one request, so if we model it as a data stream, it will be a stream with only one emitted value. Later, we know we will have many requests happening, but for now, it is just one.
--a------|-> Where a is the string 'https://api.github.com/users'
This is a stream of URLs that we want to request. Whenever a request event happens, it tells us two things: when and what. "When" the request should be executed is when the event is emitted. And "what" should be requested is the value emitted: a string containing the URL.
To create such stream with a single value is very simple in Rx*. The official terminology for a stream is "Observable", for the fact that it can be observed, but I find it to be a silly name, so I call it stream.
var requestStream = Rx.Observable.just('https://api.github.com/users');
But now, that is just a stream of strings, doing no other operation, so we need to somehow make something happen when that value is emitted. That's done by subscribing to the stream.
requestStream.subscribe(function(requestUrl) { // execute the request jQuery.getJSON(requestUrl, function(responseData) { // ... }); }
在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) { // ... }); }
Notice we are using a jQuery Ajax callback (which we assume you should know already) to handle the asynchronicity of the request operation. But wait a moment, Rx is for dealing with asynchronous data streams. Couldn't the response for that request be a stream containing the data arriving at some time in the future? Well, at a conceptual level, it sure looks like it, so let's try that.
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 }); }
What Rx.Observable.create()
does is create your own custom stream by explicitly informing each observer (or in other words, a "subscriber") about data events (onNext()
) or errors (onError()
). What we did was just wrap that jQuery Ajax Promise.Excuse me, does this mean that a Promise is an Observable?
注意到咱们这里使用的是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)?
Yes.
Observable is Promise++. In Rx you can easily convert a Promise to an Observable by doing var stream = Rx.Observable.fromPromise(promise)
, so let's use that. The only difference is that Observables are not Promises/A+compliant, but conceptually there is no clash. A Promise is simply an Observable with one single emitted value. Rx streams go beyond promises by allowing many returned values.
This is pretty nice, and shows how Observables are at least as powerful as Promises. So if you believe the Promises hype, keep an eye on what Rx Observables are capable of.
Now back to our example, if you were quick to notice, we have one subscribe()
call inside another, which is somewhat akin to callback hell. Also, the creation of responseStream
is dependent on requestStream
. As you heard before, in Rx there are simple mechanisms for transforming and creating new streams out of others, so we should be doing that.
The one basic function that you should know by now is map(f)
, which takes each value of stream A, applies f()
on it, and produces a value on stream B. If we do that to our request and response streams, we can map request URLs to response Promises (disguised as streams).
var responseMetastream = requestStream .map(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
Then we will have created a beast called "metastream": a stream of streams. Don't panic yet. A metastream is a stream where each emitted value is yet another stream. You can think of it as pointers: each emitted value is a pointer to another stream. In our example, each request URL is mapped to a pointer to the promise stream containing the corresponding response.
是的。
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数据流。
A metastream for responses looks confusing, and doesn't seem to help us at all. We just want a simple stream of responses, where each emitted value is a JSON object, not a 'Promise' of a JSON object. Say hi to Mr. Flatmap: a version of map()
than "flattens" a metastream, by emitting on the "trunk" stream everything that will be emitted on "branch" streams. Flatmap is not a "fix" and metastreams are not a bug, these are really the tools for dealing with asynchronous responses in Rx.
var responseStream = requestStream .flatMap(function(requestUrl) { return Rx.Observable.fromPromise(jQuery.getJSON(requestUrl)); });
Nice. And because the response stream is defined according to request stream, if we have later on more events happening on request stream, we will have the corresponding response events happening on response stream, as expected:
requestStream: --a-----b--c------------|-> responseStream: -----A--------B-----C---|-> (lowercase is a request, uppercase is its response)
Now that we finally have a response stream, we can render the data we receive:
responseStream.subscribe(function(response) { // render `response` to the DOM however you wish });
Joining all the code until now, we have:
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 });
一个响应的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)); });
很赞,由于咱们的响应事件流是根据请求事件流定义的,若是咱们之后有更多事件发生在请求事件流的话,咱们也将会在相应的响应事件流收到响应事件,就如所期待的那样:
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 });
I did not yet mention that the JSON in the response is a list with 100 users. The API only allows us to specify the page offset, and not the page size, so we're using just 3 data objects and wasting 97 others. We can ignore that problem for now, since later on we will see how to cache the responses.
Everytime the refresh button is clicked, the request stream should emit a new URL, so that we can get a new response. We need two things: a stream of click events on the refresh button (mantra: anything can be a stream), and we need to change the request stream to depend on the refresh click stream. Gladly, RxJS comes with tools to make Observables from event listeners.
var refreshButton = document.querySelector('.refresh'); var refreshClickStream = Rx.Observable.fromEvent(refreshButton, 'click');
Since the refresh click event doesn't itself carry any API URL, we need to map each click to an actual URL. Now we change the request stream to be the refresh click stream mapped to the API endpoint with a random offset parameter each time.
var requestStream = refreshClickStream .map(function() { var randomOffset = Math.floor(Math.random()*500); return 'https://api.github.com/users?since=' + randomOffset; });
Because I'm dumb and I don't have automated tests, I just broke one of our previously built features. A request doesn't happen anymore on startup, it happens only when the refresh is clicked. Urgh. I need both behaviors: a request when either a refresh is clicked or the webpage was just opened.
我还没提到本次响应的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; });
由于我比较笨并且也没有使用自动化测试,因此我刚把以前作好的一个功能搞烂了。这样,请求在一开始的时候就不会执行,而只有在点击事件发生时才会执行。咱们须要的是两种状况都要执行:刚开始打开网页和点击刷新按钮都会执行的请求。
We know how to make a separate stream for each one of those cases:
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');
But how can we "merge" these two into one? Well, there's merge()
. Explained in the diagram dialect, this is what it does:
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');
可是咱们是否能够将这两个合并成一个呢?没错,是能够的,咱们可使用merge()
方法来实现。下图能够解释merge()
函数的用处:
stream A: ---a--------e-----o-----> stream B: -----B---C-----D--------> vvvvvvvvv merge vvvvvvvvv ---a-B---C--e--D--o----->
It should be easy now:
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 );
There is an alternative and cleaner way of writing that, without the intermediate streams.
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 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');
The startWith()
function does exactly what you think it does. No matter how your input stream looks like, the output stream resulting of startWith(x)
will have x
at the beginning. But I'm not DRY enough, I'm repeating the API endpoint string. One way to fix this is by moving the startWith()
close to the refreshClickStream
, to essentially "emulate" a refresh click on startup.
var requestStream = refreshClickStream.startWith('startup click') .map(function() { var randomOffset = Math.floor(Math.random()*500); return 'https://api.github.com/users?since=' + randomOffset; });
Nice. If you go back to the point where I "broke the automated tests", you should see that the only difference with this last approach is that I added the startWith()
.
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()
函数而已。
Until now, we have only touched a suggestion UI element on the rendering step that happens in the responseStream'ssubscribe()
. Now with the refresh button, we have a problem: as soon as you click 'refresh', the current 3 suggestions are not cleared. New suggestions come in only after a response has arrived, but to make the UI look nice, we need to clean out the current suggestions when clicks happen on the refresh.
refreshClickStream.subscribe(function() { // clear the 3 suggestion DOM elements });
No, not so fast, pal. This is bad, because we now have two subscribers that affect the suggestion DOM elements (the other one being responseStream.subscribe()
), and that doesn't really sound like Separation of concerns. Remember the Reactive mantra?
直到如今,在响应事件流(responseStream)的订阅(subscribe()
)函数发生的渲染步骤里,咱们只是稍微说起了一下推荐关注的UI。如今有了刷新按钮,咱们就会出现一个问题:当你点击了刷新按钮,当前的三个推荐关注用户没有被清楚,而只要响应的数据达到后咱们就拿到了新的推荐关注的用户数据,为了让UI看起来更漂亮,咱们须要在点击刷新按钮的事件发生的时候清楚当前的三个推荐关注的用户。
refreshClickStream.subscribe(function() { // clear the 3 suggestion DOM elements });
不,老兄,还没那么快。咱们又出现了新的问题,由于咱们如今有两个订阅者在影响着推荐关注的UI DOM元素(另外一个是responseStream.subscribe()
),这看起来并不符合关注分离(Separation of concerns)原则,还记得响应式编程的原则么?
So let's model a suggestion as a stream, where each emitted value is the JSON object containing the suggestion data. We will do this separately for each of the 3 suggestions. This is how the stream for suggestion #1 could look like:
var suggestion1Stream = responseStream .map(function(listUsers) { // get one random user from the list return listUsers[Math.floor(Math.random()*listUsers.length)]; });
The others, suggestion2Stream
and suggestion3Stream
can be simply copy pasted from suggestion1Stream
. This is not DRY, but it will keep our example simple for this tutorial, plus I think it's a good exercise to think how to avoid repetition in this case.
如今,让咱们把推荐关注的用户数据模型化成事件流形式,每一个被发出的值是一个包含了推荐关注用户数据的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 });
Back to the "on refresh, clear the suggestions", we can simply map refresh clicks to null
suggestion data, and include that in the suggestion1Stream
, as such:
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; }) );
And when rendering, we interpret null
as "no data", hence hiding its UI element.
suggestion1Stream.subscribe(function(suggestion) { if (suggestion === null) { // hide the first suggestion DOM element } else { // show the first suggestion DOM element // and render the data } });
咱们不在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 } });
The big picture is now:
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-->
Where N
stands for null
.
As a bonus, we can also render "empty" suggestions on startup. That is done by adding startWith(null)
to the suggestion streams:
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);
Which results in:
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-->
如今咱们的一个大的示意图是这样的:
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-->
There is one feature remaining to implement. Each suggestion should have its own 'x' button for closing it, and loading another in its place. At first thought, you could say it's enough to make a new request when any close button is clicked:
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; });
That does not work. It will close and reload all suggestions, rather than just only the one we clicked on. There are a couple of different ways of solving this, and to keep it interesting, we will solve it by reusing previous responses. The API's response page size is 100 users while we were using just 3 of those, so there is plenty of fresh data available. No need to request more.
只剩这一个功能没有实现了,每一个推荐关注的用户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个用户数据,而咱们只使用了其中三个,因此还有一大堆未使用的数据能够拿来用,不用去请求更多数据了。
Again, let's think in streams. When a 'close1' click event happens, we want to use the most recently emitted response onresponseStream
to get one random user from the list in the response. As such:
requestStream: --r---------------> responseStream: ------R-----------> close1ClickStream: ------------c-----> suggestion1Stream: ------s-----s----->
In Rx* there is a combinator function called combineLatest
that seems to do what we need. It takes two streams A and B as inputs, and whenever either stream emits a value, combineLatest
joins the two most recently emitted values a
and b
from both streams and outputs a value c = f(x,y)
, where f
is a function you define. It is better explained with a diagram:
stream A: --a-----------e--------i--------> stream B: -----b----c--------d-------q----> vvvvvvvv combineLatest(f) vvvvvvv ----AB---AC--EC---ED--ID--IQ----> where f is the uppercase function
ok,再来,咱们继续用事件流的方式来思考。当'close1'点击事件发生时,咱们想要使用最近发出的响应数据,并执行responseStream
函数来从响应列表里随机的抽出一个用户数据来,就像下面这样:
requestStream: --r---------------> responseStream: ------R-----------> close1ClickStream: ------------c-----> suggestion1Stream: ------s-----s----->
在Rx中一个组合函数叫作combineLatest
,应该是咱们须要的。这个函数会把数据流A和数据流B做为输入,而且不管哪个数据流发出一个值了,combineLatest
函数就会将从两个数据流最近发出的值a
和b
做为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是转换成大写的函数
We can apply combineLatest() on close1ClickStream
and responseStream
, so that whenever the close 1 button is clicked, we get the latest response emitted and produce a new value on suggestion1Stream
. On the other hand, combineLatest() is symmetric: whenever a new response is emitted on responseStream
, it will combine with the latest 'close 1' click to produce a new suggestion. That is interesting, because it allows us to simplify our previous code for suggestion1Stream
, like this:
var suggestion1Stream = close1ClickStream .combineLatest(responseStream, function(click, listUsers) { return listUsers[Math.floor(Math.random()*listUsers.length)]; } ) .merge( refreshClickStream.map(function(){ return null; }) ) .startWith(null);
One piece is still missing in the puzzle. The combineLatest() uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, combineLatest() cannot produce a data event on the output stream. If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value a
. Only when the second stream emitted valueb
could it produce an output value.
There are different ways of solving this, and we will stay with the simplest one, which is simulating a click to the 'close 1' button on startup:
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);
这样,咱们就能够把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);
One piece is still missing in the puzzle. The combineLatest() uses the most recent of the two sources, but if one of those sources hasn't emitted anything yet, combineLatest() cannot produce a data event on the output stream. If you look at the ASCII diagram above, you will see that the output has nothing when the first stream emitted value a
. Only when the second stream emitted valueb
could it produce an output value.
There are different ways of solving this, and we will stay with the simplest one, which is simulating a click to the 'close 1' button on startup:
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);
如今,咱们的拼图还缺一小块地方。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);
And we're done. The complete code for all this was:
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 } });
咱们完成了,下面是封装好的完整示例代码:
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 } });
You can see this working example at http://jsfiddle.net/staltz/8jFJH/48/
That piece of code is small but dense: it features management of multiple events with proper separation of concerns, and even caching of responses. The functional style made the code look more declarative than imperative: we are not giving a sequence of instructions to execute, we are just telling what something is by defining relationships between streams. For instance, with Rx we told the computer that suggestion1Stream
is the 'close 1' stream combined with one user from the latest response, besides being null
when a refresh happens or program startup happened.
Notice also the impressive absence of control flow elements such as if
, for
, while
, and the typical callback-based control flow that you expect from a JavaScript application. You can even get rid of the if
and else
in the subscribe()
above by using filter()
if you want (I'll leave the implementation details to you as an exercise). In Rx, we have stream functions such asmap
, filter
, scan
, merge
, combineLatest
, startWith
, and many more to control the flow of an event-driven program. This toolset of functions gives you more power in less code.
你能够在这里看到可演示的示例工程
以上的代码片断虽小但作到不少事:它适当的使用关注分离(separation of concerns)原则的实现了对多个事件流的管理,甚至作到了响应数据的缓存。这种函数式的风格使得代码看起来更像是声明式编程而非命令式编程:咱们并非在给一组指令去执行,只是定义了事件流之间关系来告诉它这是什么。例如,咱们用Rx来告诉计算机suggestion1Stream
是'close 1'事件结合从最新的响应数据中拿到的一个用户数据的数据流,除此以外,当刷新事件发生时和程序启动时,它就是null
。
留意一下代码中并未出现例如if
, for
, while
等流程控制语句,或者像JavaScript那样典型的基于回调(callback-based)的流程控制。若是能够的话(稍候会给你留一些实现细节来做为练习),你甚至能够在subscribe()
上使用 filter()
函数来摆脱if
和else
。在Rx里,咱们有例如: map
, filter
, scan
, merge
, combineLatest
, startWith
等数据流的函数,还有不少函数能够用来控制事件驱动编程(event-driven program)的流程。这些函数的集合可让你使用更少的代码实现更强大的功能。
If you think Rx* will be your preferred library for Reactive Programming, take a while to get acquainted with the big list of functionsfor transforming, combining, and creating Observables. If you want to understand those functions in diagrams of streams, take a look at RxJava's very useful documentation with marble diagrams. Whenever you get stuck trying to do something, draw those diagrams, think on them, look at the long list of functions, and think more. This workflow has been effective in my experience.
Once you start getting the hang of programming with Rx, it is absolutely required to understand the concept of Cold vs Hot Observables. If you ignore this, it will come back and bite you brutally. You have been warned. Sharpen your skills further by learning real functional programming, and getting acquainted with issues such as side effects that affect Rx.
But Reactive Programming is not just Rx. There is Bacon.js which is intuitive to work with, without the quirks you sometimes encounter in Rx. The Elm Language lives in its own category: it's a Functional Reactive Programming language that compiles to JavaScript + HTML + CSS, and features a time travelling debugger. Pretty awesome.
Rx works great for event-heavy frontends and apps. But it is not just a client-side thing, it works great also in the backend and close to databases. In fact, RxJava is a key component for enabling server-side concurrency in Netflix's API. Rx is not a framework restricted to one specific type of application or language. It really is a paradigm that you can use when programming any event-driven software.
If this tutorial helped you, tweet it forward.
若是你认为Rx将会成为你首选的响应式编程库,接下来就须要花一些时间来熟悉一大批的函数用来变形、联合和建立被观察者。若是你想在事件流的图表当中熟悉这些函数,那就来看一下这个:RxJava's very useful documentation with marble diagrams。请记住,不管什么时候你遇到问题,能够画一下这些图,思考一下,看一看这一大串函数,而后继续思考。以我我的经验,这样效果颇有效。
一旦你开始使用了Rx编程,请记住,理解Cold vs Hot Observables的概念是很是必要的,若是你忽视了这一点,它就会反弹回来并残忍的反咬你一口。我这里已经警告你了,学习函数式编程能够提升你的技能,熟悉一些常见问题,例如Rx会带来的反作用
可是响应式编程库并不只仅是Rx,还有相对容易理解的,没有Rx那些怪癖的Bacon.js。Elm Language则以它本身的方式支持响应式编程:它是一门会编译成Javascript + HTML + CSS的响应式编程语言,并有一个time travelling debugger功能,很棒吧。
而Rx对于像前端和App这样须要处理大量的编程效果是很是棒的。可是它不仅是能够用在客户端,还能够用在后端或者接近数据库的地方。事实上,RxJava就是Netflix服务端API用来处理并行的组件。Rx并非局限于某种应用程序或者编程语言的框架,它真的是你编写任何事件驱动程序,能够遵循的一个很是棒的编程范式。
若是这篇教程对你有帮助, 那么就请来转发一下吧(tweet it forward).