【翻译】你错过的响应式编程

(原文: @andrestaltz,翻译:Mingfei Ding)


你一定对响应式编程很好奇,特别是他的一些变体:包括 Rx, Bacon.js, RAC。

学习的过程是困难的,如果缺乏好的学习材料会更难。在我刚开始学习的时候,我找了许多教程,只发现少数实用的,而且他们还只是抓了表面,从未深入整个架构。文档常常对你理解函数意义没有多大帮助,就像这样:

Rx.Observable.prototype.flatMapLatest(selector, [thisArg])
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.

喵喵喵?

我都过两本书,一本只是画了一个大饼,而另一本,则是深入如何使用响应库。我最终使用这种方式学习响应式编程:在用它的同时理解它。当我在Futurice工作的时候,我得以在一个实际项目中使用它,并且当我遇到困难时,得到了同事们的帮助。

学习过程中最难的一部分在于”Thinking in Reactive”。就是说要放弃原来编程中xx一些旧的命令和习以为常的惯例xx,并且强迫自己的大脑用一种不同的范式工作。我没有在网上找到过一个
这方面的教程,而且认为这个世界需要一个实用的关于如何”Thinking in Reactive”的教程,那么你可以开始了,我希望看过这篇文章之后,会对你看文档有帮助。

“什么是响应式编程”

网上有许多不好的解释和定义,维基百科太过泛泛和理论性,Stackoverflow规范的答案很明显不适合新手,响应式宣言听起来像是给产品经理听的。微软的Rx terminology“Rx = Observables + LINQ + Schedulers”是一个典型的微软风重量级的库,让我们大多数人难以理解。像“响应式”,“传递改变”这种属于,没有传达出你用MVX风的库有丁点的区别。当然,我们的框架也是视图相应模型,也是传递改变的,如果不这样,没有什么可以渲染出来的。

那么,让我们解剖一下这吨翔。

###响应式编程是对异步数据流编程

在这种程度上而言,没有什么新东西。事件总线或者你的点击事件都是真正的异步事件流,在那上面,你可以观察并且做一些额外的事。响应式就是那个nb的东西。你可以为任何东西创造数据流,而不是仅仅为点击或者悬浮事件。流是很普及的,任何东西都可以成为一个流:变量,用户输入,属性,缓存,数据结构等等。例如,想象一下,你微博上的推送像点击事件一样都是数据流吧。你可以监听它,并根据它做出反应。

最重要的是,你得到了一个合并,产生,过滤这些流的函数利器,那就是功能性魔法的地方。一个流可以作为另一个流的输入,甚至多个流可以被当作另一个流的输入。你可以合并两个流。你可以过滤一个流得到另一个只有你感兴趣事件的流,你可以map数据值,从一个流到另一个流。

既然数据流在响应式中如此的重要,让我们细致观察一下他们,就从我们熟悉的“点击一个按钮”事件流开始。
点击按钮事件流

流是将要发生的事件按照时间排序的序列。它可以发射3个不同的东西:值(某种类型的),错误,或者是一个“完成”信号。考虑一下“完成”什么时候发生,例如,当现有包含那个按钮的窗口或者视图关闭的时候。

我们通过定义一个当值被发出时执行的函数,一个当错误被抛出时执行的函数和一个当“完成”被发出时的函数,来捕获这些发生的事件。有时,后两个可以被省略,因此你可以仅仅关注如何定义第一个函数。对流的“监听”叫做订阅(subscribing)。我们定义的函数叫做观察者(observers)。流就是被观察的主体。这就是观察者模式。

一个画图的替代品是用ASCII画一个图表,我们后面也会在用到它。

1
2
3
4
5
6
--a---b-c---d---X---|->

a, b, c, d are emitted values
X is an error
| is the 'completed' signal
---> is the timeline

因为我们已经对它非常熟悉了,我也不想你无聊,所以让我们做一些新的事:我们将创建从原始点击事件流转换出的新的点击事件流。

首先,让我们做一个计数流,用来指示你点了多少次按钮。在常见的响应库中每个流都附有很多函数,比如code, filter, scan等等,当你调用这些函数的时候,比如clickStream.map(f),他返回了一个基于原流的新流。它没有修改原流。这个性质成为不变性。它和响应流搭配就像是煎饼卷大葱。它可以让函数写成函数链的形式,就好像clickStream.map(f).scan(g)

1
2
3
4
5
  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(accumulate, current),g是一个简单的加法函数。最终,当你触发点击事件时,counterStream发射了一个点击总数值。

为了展示响应式的真正实力,我们来说说双击事件。为了让他更有趣一点,我们把三击事件也视为双击事件,或者更广泛的说,多击事件都算作双击事件。考虑一下,如果放在以前,实现起来会是什么样的。我保证这听起来很恐怖,而且得用很多变量来保存变量和间隔。

但是,在响应式编程中,事实上,逻辑仅仅需要四行代码。但是现在让我们无视这些代码,看图思考是理解这些流的最好方式。
Click stream

灰盒子中是转换函数。首先,简而言之,每当250毫秒的“沉默事件”发生(就是buffer(stream.throttle(250ms)。别担心不理解细节,我们现在只是举个栗子),我们累计了列表中的点击事件。结果返回一个列表的流,我们map一发这整个流中的列表,并把它转化成列表长度对应的值。最终,我们利用filter(x >= 2)函数忽略整数1,这就是生成我们想要的流的三个函数,然后我们订阅这个监听事件,得到我们想要得到的反应。

我希望你可以享受这个方法的优雅,这个例子只是冰山一角,你可以实现它玩玩。

“为什么我需要考虑采用响应式编程?”


反应式编程提高了代码的抽象级别,因此您可以专注于定义业务逻辑的事件的相互依赖关系,而不必不断地解决大量的实现细节。 RP中的代码会更简洁。

在与数据事件相关的大量UI事件高度交互的现代webapps和移动应用程序中,优势更加明显。 10年前,与网页的互动基本上是向后端提交一个长表单,并向前端执行简单呈现。 应用程序已经演变成更实时:修改单个表单域可以自动触发后端保存,“点赞”等内容可以实时反映给其他在线的用户,等等。

现在的应用程序拥有丰富的各种实时活动,能够为用户提供高度互动的体验。 我们需要正确处理的工具,而响应式编程就是一个答案。

##RP编程思想及实例

让我们到现实中来。一个现实世界关于如何以响应式编程的思想思考问题的起步例子。没有xx合成xx的例子,没有解释不全的概念。 在本教程结束之前,我们将会写出真正有效的代码,同时知道我们做每件事是为什么。

##实现一个“关注谁”推荐框
twitter的是这样的:
Twitter推荐框

我们将实现核心功能:

  • 打开的时候,从API载入账号信息,并展示出三个推荐
  • 点击刷新的时候,载入三个新的推荐账号
  • 点击x的时候,清除当前账号,并展现出另一个
  • 每条都展示用户头像和他们的网站链接

我们将用Github API展示。
在线完整示例:http://jsfiddle.net/staltz/8jFJH/48

##请求和响应


你将如何用Rx解决这个问题?,首先,(几乎)所有东西都可以是一个流,这是Rx的信条。让我们从这个最简单的特性开始:“在启动的时候,从API载入三个账号信息”。没什么特别的,(1)请求,(2)得到响应,(3)渲染响应。那么让我们用一个流展现我们的请求,开始我们可能觉得这个东西杀伤力巨大,但我们也要从最基本的开始。

首先我们要先做一个请求,如果我们把他建模成一个数据流,他是一个仅仅有一个发射值的流,然后,我们知道我们会有很多请求,但是现在只有一个。

1
2
3
--a------|->

Where a is the string 'https://api.github.com/users'

这是一个我们想请求的URL流,无论请求事件何时发生,都告诉我们两件事情,何时和何事。“何时”请求被执行就是事件发射的时间。“何事”应该被请求就是被发射的值:一个包含URL的字符串。

创造一个这样的流在Rx中非常简单,流的官方名字叫做“可观察对象(Observable)”,因为它可以被观察,但是我觉得这个名字有点sb,所以我还是叫他流。

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

但是现在,那只是一个字符串流,并没有做其他的操作,所以当值被发射时,我们应该做点什么。订阅(Subscribing)解决的这个问题。

1
2
3
4
5
6
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}

注意到我们用jQuery Ajax回调来解决这个异步的请求操作。但是等一等,Rx就是为了解决异步数据流的。我们不能用点新技术吗?我们来尝试一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
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( ))或是错误(errors( ))创造了你自己的流。我们做的就是包装了jQuery Ajax 承诺。等等,你意思是承诺也是一个可观察对象?

没错。
可观察对象就是升级版的Promise。在Rx中你可以通过var stream = Rx.Observable.fromPromise(promise)轻易地转换Promise=>Observable。唯一的区别是Observable不兼容 Promises/A+规范,但是在概念上没有区别。Promise是单个发射值的Observable,Rx流允许多个返回值。

那就非常棒了,至少展示了Observable和Promise一样强力。所以如果你相信Promise,也请你相信Rx有这个能力。

回到我们的例子中来,就像你注意到的,我们把一个subscribe()套在另一个当中,这有点像回调地狱。而且,responseStream是依赖requeststream的。正如你之前听到的,在Rx中,具有很简单的转换和生成新的流的方法,因此,我们应该那样实现它。

有一个你应该知道的基础函数是map(f),他让流A中的所有值都通过f( )函数,并且返回一个新的流。如果我们在req和res流中使用它,我们可以map URLs =》返回的Promise。

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

然后我们创造了一个叫“元流”的东西,一个流的流。莫惊慌,元流是一个流,他的每个发射值也是一个流。你可以把它想象成一个指针。每一个发射值都是一个流的指针。在我们的例子里,每一个请求URL都 =》一个指向包含相关响应的一个Promise流的指针。
Request stream

返回的元流看起来有点令人费解,而且好像没帮到我们什么忙,我们只想要一个发射JSON对象的返回流,而不是一个Promise。那么我们就要用到flatMap了:一个可以“扁平”元流的map( )版本,通过把“支流”上发送的所有东西,都在“主流上发送”,来扁平化数据发送。Flatmap并不是用来”fix” Metastream的,因为Metastream也不是一个Bug,这只是一些用来处理Rx中的异步响应(Asynchronous response)的工具。

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

req stream

很棒,因为响应流是通过请求流定义的,如果我们后面有更多的请求,我们会有更多的响应事件发生在响应流上,就像预期的一样。

1
2
3
4
5

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

(lowercase is a request, uppercase is its response)

现在我们可以渲染数据了:

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

把代码都放在一起:

1
2
3
4
5
6
7
8
9
10
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个用户的列表。这个API只允许我们设置页面偏移,而不是页面的大小,所以我们用了三个数据,浪费了剩下的97个。我们现在先不管这个事,后面在研究如何缓存这些响应。

每次刷新键按下的时候,请求流应该发射一个新的URL,然后我们得到一个新的响应。我们需要两件事:1.一个刷新按钮点击事件流(任何东西都可以是一个流),2.我们需要改变请求流,让他以来刷新点击事件流。幸运的是,Rx有直接帮我们完成这些的、基于事件监听的工具。

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

因为刷新按钮不会自己携带API URL,所以我们需要map click=》实际的URL。现在我们把req stream改成当刷新按钮点击的时候,请求加上一个随机的偏移。

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

因为我很笨,而且我也没有自动测试,我打破了先前版本的一些特性。请求不再在刚打开页面的时候发生了,它仅仅在刷新的时候发生。呃…这两者我都需要啊。

我们知道如何分别写两个流:

1
2
3
4
5
6
7
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( )。用图表来解释,就是这样:

1
2
3
4
5

stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->

那就很简单了啊:

1
2
3
4
5
6
7
8
9
10
11
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
);

另一个不用中间流的更清晰的方法是:

1
2
3
4
5
6
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'));

更短、更好读的:

1
2
3
4
5
6
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。一个改善的方法是把startWith()移动到refreshClickStream里面,在启动时模拟一次刷新点击。

1
2
3
4
5
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()渲染了一个推荐元素。现在有了刷新按钮,我们有了一个问题,在你点击‘刷新’的时候,现有的三个推荐没有被clear掉。新的推荐只有在响应返回是出现,但是为了让UI更好看一些,当点击刷新的时候,我们需要清除现有的3个推荐。

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

不,别这么快,朋友,这不好,因为我们有两个订阅者影响着推荐DOM元素(另一个是responseStream.subscribe()),而且听起来也不符合Separation of concerns,还记得响应式信条吗?

everything is a stream

我们用流来建模一个推荐,流中每一个发射值都是一个包含着推荐信息的JSON对象。我将会为3个推荐分别做这件事,这就是推荐#1的样子:

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

另外两个推荐可以简单地复制粘贴,但这不清真,但是可以让我们的例子看简单,而且我觉得让你自己去思考如何避免重复更好。

我们不在responseStream的subscribe( )里渲染了,我们在suggestion1Stream的subscribe( )里渲染:

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

回到刚才的“只要刷新,就清除推荐”历来,我们可以map 刷新click=》null,并把它包含到suggestion1Stream中,就像这样。

1
2
3
4
5
6
7
8
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元素。

1
2
3
4
5
6
7
8
9
suggestion1Stream.subscribe(function(suggestion) {
if (suggestion === null) {
// hide the first suggestion DOM element
}
else {
// show the first suggestion DOM element
// and render the data
}
});

现在,图表是这样的了:

1
2
3
4
5
6
7

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)添加到建议流中来完成:

1
2
3
4
5
6
7
8
9
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);

结果是:

1
2
3
4
5
6
7

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

关闭一个推荐和缓存响应


还有一个特性没有实现,每个推荐应该都有一个×按钮来关掉它,并且在原位置打开一个新的。乍一想,你可能觉得点击×的时候,做一个新的请求就够了:

1
2
3
4
5
6
7
8
9
10
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个user,但是我们只需要三个,所以有很多可用的数据,不用再请求了。

再一次让我们用流来考虑问题。当我们点击‘close1’时,我们想用最近一次responseStream发射的响应得到一个随机的user,就像这样:

1
2
3
4
5

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

在Rx *中,有一个名为combineLatest的组合函数似乎是我们需要的。他以两个流A和B为输入,无论任意一个流何时发送数据,combineLatest结合两个流的发射值a,b得到结果c=f(x,y)f是你自己定义的函数,用图表来解释更清晰:

1
2
3
4
5
6
7

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

我们可以在close1ClickStreamresponseStream上应用combineLatest()函数,无论关闭1按钮何时被点击,我们都得到suggestion1Stream发射的一个新值。换句话说,combineLatest()是对称的:无论何时在responseStream上发出一个新的响应,他都会把最近的关闭1事件绑定到一个新的推荐上。那很有趣,因为他让我们简化了我们之前的suggestion1Stream代码,就像这样:

1
2
3
4
5
6
7
8
9
10
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()无法在Output stream中产生一个Data event。从上边的ASCII图中,你可以看到,在第一个Stream emit a这个值时并没有任何输出产生,只有当第二个Stream emit b时才有值输出。

有多种方法可以解决这个问题,我们选择最简单的一种,一开始在’close 1’按钮上模拟一个点击事件:

1
2
3
4
5
6
7
8
9
10
var suggestion1Stream = close1ClickStream.startWith('startup click') // we added this
.combineLatest(responseStream,
function(click, listUsers) {
return listUsers[Math.floor(Math.random()*listUsers.length)];
}
)
.merge(
refreshClickStream.map(function(){ return null; })
)
.startWith(null);

总结


我们做完了,全部的代码是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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
}
});

你可以查看这个最终效果 http://jsfiddle.net/staltz/8jFJH/48/

这段代码虽然短小,但实现了不少功能:它适当的使用Separation of concerns实现了对Multiple events的管理,甚至缓存了响应。函数式的风格让代码看起来更加Declarative而非Imperative:我们并非给出一组指令去执行,而是通过定义Stream之间的关系 定义这是什么。举个例子,我们使用Rx告诉计算机 _suggestion1Stream 由 ‘close 1’ Stream与最新响应中的一个用户合并(combine)而来,在程序刚运行或者刷新时则是null_。

留意一下代码中并没有出现如ifforwhile这样的控制语句,或者一般JavaScript应用中典型的基于回调的控制流。如果你想使用filter(),上面的subscribe()中甚至可以不用ifelse(实现细节留给读者作为练习)。在Rx中,我们有着像mapfilterscanmergecombineLateststartWith这样的Stream函数,甚至更多类似的函数去控制一个事件驱动(Event-driven)的程序。这个工具集让你可以用更少的代码实现更多的功能。

下一步

如果你觉得Rx*会成为你首选的RP库,花点时间去熟悉这个函数列表,包括了如何转换(transform)、合并(combine)、以及创建Observable。如果你想通过图表去理解这些函数,看一下这份RxJava’s very useful documentation with marble diagrams。无论什么时候你遇到问题,画一下这些图,思考一下,看一下这一大串函数,然后继续思考。以我个人经验,这样效果很明显。

一旦你开始使用Rx去编程,很有必要去理解Cold vs Hot Observables中的概念。如果忽略了这些,你一不小心就会被它坑了。我提醒过你了。通过学习真正的函数式编程(Funational programming)去提升自己的技能,并熟悉那些会影响到Rx的问题,比如副作用(Side effect)。

但是RP不仅仅有Rx。还有相对容易理解的Bacon.js,它没有Rx那些怪癖。Elm Language则以它自己的方式支持RP:它是一门会编译成Javascript + HTML + CSS的FRP 语言,并有一个Time travelling debugger。非常NB。

Rx在需要处理大量事件的Frontend和Apps中非常有用。但它不仅仅能用在客户端,在Backend或者与Database交互时也非常有用。事实上,RxJava是实现Netflix’s API服务器端并发的一个重要组件。Rx并不是一个只能在某种应用或者语言中使用的Framework。它本质上是一个在开发任何Event-driven软件中都能使用的编程范式(Paradigm)。

如果这份教程能帮到你,请与更多人分享

原文地址