原文 The introduction to Reactive Programming you’ve been missing

Reactive编程就是异步数据流的编程。

总的来说,这里没有什么新东西。事件总线或者典型的click事件就是一种异步事件流,你可以观察它们并做出一些响应。Reactive就是基于这种理论。你可以创建任何事物的数据流。流的代价很低而且很普遍,任何事物都可以是流:变量,用户输入,属性 ,缓存,数据结构等等。比如Twitter的消息源可以看作是相应点击事件的数据流。你可以监听它们并作出相应的响应。

除此之外,它们提供了一个用于连接,创建,过滤任何流的函数库。不仅某个流可以用于另一个流的输入,多个流同样可以作为其它流的输入。你也可以合并两个流。如果你对某些事件感兴趣,也可以通过对一个流的过滤获得另一个目标流。也可以将一个流中的数据映射到一个新的数据流。

可以看到流对Reactive来说有多重要,我们仔细来看一下它们,以我们熟悉的按钮点击事件流开始:

一个流是将要发生的有序序列事件的一部分。它可以发出三种不同的事件:value,error或者completed。假想下“completed”事件的场景,比如window或者view中的关闭按钮点击的时候。

我们所谈到的事件只会是异步的,通常会定义三个要执行的函数,一个在值输入的时候发生,一个在错误发生时执行,以及一个事件完成时执行。有时候后面两个会被省略,你只需要定义输入值的处理函数,对于流的监听被称作订阅。我们定义的函数被称作观察者。流(或者Observable)是被观察的对象。在观察者模式中写的很明确:Observer Design Pattern.

一种转换的方式是通过ASCII图来表示,在本教程的多处地方我们都会用到:

1
2
3
4
5
6
--a---b-c---d---X---|->
a, b, c, d 代表分发的值
X 代表错误
| 代表事件完成信号
---> 代表时间线

既然我们对一些概念比较熟悉了,那咱么就尝试点新的东西:我们将创建一个新的点击事件流,该事件流由原始的点击事件流演化而来。

首先,我们创建一个计数流,用于计算一个button点击了多少次。通常在Reactive库中,每一个流都附带很多函数,比如map,filter,scan等等。当你调用其中任意一个函数,比如clickStream.map(f),会基于当前的click流返回个一个新的流。但不管怎样,它都不会修改原始的click流。这种特性称作不变性,这种不变性会把Reactive流组织在一起。这样做可以实现链式调用,比如: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映射到了每一个click事件上。scan(g)函数叠加了流上的所有值,通过 x = g(accumulated, current)得到x,g代表示例中的”+”函数。然后,counterStream会分发点击事件发生的总数。

为了展示Reactive的强大,假设我们需要一个双击事件流。为了看起来更有趣,假设我们的新流能够考虑到类似双击多次的点击事件。考虑下如果通过传统的,不可避免的状态性方式来实现它。我打赌那样的实现方式看起来一定不爽,并且引入了一些用于保持状态和时间间隔的变量。

在Reactive中实现起来是非常简洁的。这个逻辑只需要4行代码就可搞定。现在我们先暂时忽略代码。让我们来用图表来思考,不管你是初级还是专家,使用图标来思考都是最好的理解和构建流的方式。

灰色部分的函数将一个流转换成了另一个流。首先我们将列表中的clicks事件的间隔为250毫秒的事件计算并归类。先不考急着理解细节的实现,到现在我们只是展示了Reactive的使用。我们得到的结果是一个列表的流,从这个列表流中我们可以运用map()将每一个list中的length映射成一个integer。最终我们通过filter( x >= 2 )函数过滤掉integer = 1的值。结果有3个操作满足我们的意图流。然后我们可以通过subcribe订阅它并作出期望的响应。

希望你喜欢这种形象的讲解方式。这个示例只是一个简单的引子,你可以将这种方式应用到任何种类的流中,比如在一个API响应的流中,或者其它可用的函数。

#为什么我应该考虑采用Reactive 编程?
反应式编程提高了代码抽象的层次,你可以聚焦于定义了业务逻辑的事件依赖,而不是被大量的实现细节所困扰。Reactive编程会让代码更简洁。

在如今的多UI事件与数据事件高度交互的webapp和移动app中,这种好处会更加的明显。10年前,与web界面的交互是通过向后台提交长表单并在前端进行简单的渲染。随着演变,App的响应更加实时:修改一个单独的表单字段可以自动的触发后台的保存,某些用户收藏的内容可以及时反映给其他相关的用户。

如今的app拥有各种各样用于增强交互体验的实时事件。我们需要处理这种情景的工具,反应式编程就是很好的解决方案。

#以RP的方式思考

让我们来点干货。通过真实的例子一步步的探索如何以PR的方式思考问题。没有捏造的例子,没有模糊不清的概念。在教程的结尾我们会产出真实的代码,同时了解每一步的操作为什么这么做。

我选用了JS和RxJs作为工具。原因是JS是如今最常见的语言。并且Rx library大家庭可以广泛的适用于多种语言和平台 (.NET, Java, Scala, Clojure, JavaScript, Ruby, Python, C++, Objective-C/Cocoa, Groovy, etc),因此不管你使用什么工具,你都可以从下面的教程中受益。

#实现一个“Who to follow”建议模块

在Twitter中,有一个模块提供了你可能感兴趣的建议用户

我们将要关注该模块以下核心特性:

  • 当启动的时候,从API加载用户数据并展示3条建议
  • 点击“刷新”,更新三条新的用户建议
  • 点击“x”按钮,清理当前展示的那条用户建议
  • 每一行展示的用户头像和相应的twitter链接

没有选择其它的特性和按钮是因为它们和我们的目的关系不是很紧密。但Twitter最近向未授权的第三方关闭了API,这里通过Github的following poeple的API来代替。这里是获取Github 用户的 API

完整的代码已经有了: http://jsfiddle.net/staltz/8jFJH/48/

#请求和响应
如何使用Rx解决问题?首先你得明白,“一切皆流”,这是Rx的核心理念。回到一开始提到的特性:“启动的时候从API加载3个用户数据”。这个很简单,简单来说就三步:请求,获取结果,渲染结果。首先我们把请求作为一个流。乍一看感觉有点手足无措,但我们可以先从最基础的开始:

1
2
3
--a------|->
a 代表 string 'https://api.github.com/users'

这个URL流是我们要去请求的。不管请求何时发生,它会告诉我们两件事:when和what。“when”当事件被分发时请求执行的时间。“what”就是请求要分发的值:一个包含URL的string。

在Rx中创建这样只有一个值的流非常简单。在官方的术语中,流就是“Observable”,可以被观察的,但是我觉得这个名字很傻瓜(不够形象),所以我把它称作stream。

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

现在,这里只是一个string流,还没有任何其它的操作。因此当值分发的时候,我们需要做一些处理,这些处理会被订阅到流中。

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
14
15
16
17
18
19
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(这里对Promise不做翻译,是因为对JS中的概念不熟,暂且理解为“期望”吧)就是一个Observable?

是的!
Observable就是Promise++。在Rx中,你可以通过

1
var stream = Rx.Observable.fromPromise(promise)

简单的将一个Promis转换为一个Observable。唯一不同的区别就是Observables不是 Promises/A+,但概念并不冲突。一个Promise就是一个简单的带有分发值的Observable。但Rx流允许很多返回值,在这一点上,Rx要超过promises。

真是太棒了。说明Observable至少和Promises一样强大。如果你对Promises很赞赏,那么你可以关注Rx Observables都适用于哪些部分。

让我们回到之前的例子。你可能早就注意到,我们在另一个流中调用了subscribe(),一个被称作回调地狱的处理。当然,responseStream的创建依赖于requestStream。正如你之前看到的那样,Rx拥有简单的机制用于转换和创建流自身之外的流,这也是我们将要做的。

到目前我们所知道的一个基础函数就只有 map(f) ,它获取了stream A的每一个值。通过应用 f( ) ,可以在stream B上产生一个值。如果要把这样的处理应用于我们的请求和响应流,我们可以把请求的URLs转换为响应的Promises(可以假想为流)。

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

接着我们将会创建一个称作“metstream”的东西:流中的一种,别慌张啊,一个metastream就是一个steam,只是它的每一个分发值是另一个流。你可以把它看作指针:每一个分发值就是一个指向其它stream的指针。在我们的例子中,每一个请求URL被映射到一个指向其它包含了响应的流指针。

响应的metastream可能会让人有点迷惑,看起来对我们一点帮助也没有。我们只是期望一个简单的响应stream,stream中每一个值是一个JSON对象,而不是一个Json对象的“Promise”。让我们来认识下Flatmap吧,Flatmap是map()的一种,它能够会“铺平“一个metastream,从干流上把流分发到每一个分支上。

Flatmap不是一个修复,metastream当然也不是一个bug。它们只是Rx中处理异步流的工具。

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

太棒了。同时因为响应流是根据请求流定义的,如果之后我们有更多事件发生在请求流中,也期望在响应流中作出相应的响应事件,比如:

1
2
3
4
5
requestStream: --a-----b--c------------|->
responseStream: -----A--------B-----C---|->
小写字母是请求,大写字母是响应

现在我们拥有了一个响应流,可以渲染数据了:

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只允许我们指定page offset,而不是page size,因此我们只使用3条数据对象,剩下的97条都浪费了。我们可以暂时忽略这部分,因为之后我们会缓存这个response。

每次刷新按钮点击的时候,请求流会分发一个新的URL,因此我们会得到一个新的response。我们需要做两件事:一个刷新按钮的click事件流(一切皆流),我们需要将请求流转换为一个依赖于刷新事件流的流。RxJs提供了从事件监听者创建Observable的工具。

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

由于刷新事件自己不含有任何API URL,我们需要将每一个click事件映射成一个实际的URL。现在我们把请求流转变成每一次都带有任意offset参数API的刷新点击流。

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

因为我比较菜而且没有进行自测,我把之前要做的特性搞砸了。现在请求在启动之前不会发生,而只有点击刷新按钮的时候才发生。现在我需要满足这两种行为 :刷新按钮点击的时候和web界面打开的时候。

我们知道如何从上面两种情况分别构建一个独立的流:

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”成一个流呢?没错,就是merge方法,下面的图标中解释merge所做的:

1
2
3
4
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 (Don’t Repeate Yourself),我这里重复了API string啊。一种改善的方式是通过移除refreshClickStream附近的startWith()方法,在方法开始的时候模拟一个刷新按钮的点击。

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()方法。

#通过streams构建3条建议的模型
直到现在,在响应流的subscribe()方法的渲染步骤中,我们只接触到了一个suggestion UI元素。现在的刷新按钮,存在一个问题:如果尽可能快的点击“refresh”,当前的3条建议还没来得及被清除掉,新的建议就已经会伴随新的请求结果出现了,但是为了让UI看起来更漂亮,当刷新的点击事件发生的时候,我们需要清理当前的建议。

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

别急,其实这么做是不对的。因为现在有两个订阅者影响了suggesion节点元素(另一个通过responseStream.subscribe()订阅),并且看起来不那么的Separation of concerns(根据作者的意图来看,这里的意思是尽量让事件更独立和明确,这里把影响suggestion的两个订阅者分离开) 。还记得Reactive的准则么?

一切皆流

我们把一条建议模型化为一个流,在这里每一个分发值是一个包含建议数据的JSON对象。我们将3条建议分开,下面是第一条建议流:

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

建议2,建议3的流可以简单的直接从上面copy。这不能称作DRY,但它能够让我们的教程示例更简单,当然考虑如何避免这样的重复操作会是一个不错的实践。

要在response流的subscribe方法中进行渲染操作可以这样:

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

回到之前的需求:“点击刷新,删除建议”,我们可以将点击刷新简单的映射成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
8
9
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

有了上面的做法,我们同样可以在开始的时候渲染suggestion为empty的情况。通过添加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条用户,而我们只需要三条,因此有大量的新的可用数据,而不需要再次请求。

我们再以流的方式思考下。当第一个按钮点击事件发生时候,我们希望从responseStream最近一次分发的response列表中任意获取一条user数据,比如:

1
2
3
4
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
stream A: --a-----------e--------i-------->
stream B: -----b----c--------d-------q---->
vvvvvvvv combineLatest(f) vvvvvvv
----AB---AC--EC---ED--ID--IQ---->
f 代表大写函数

我们可以将combineLatest()应用于closeClickStream和responseStream,因此只要当第一个关闭按钮被点击了,我们就可以获取到最新的结果,该结果分发并产生个一个新的值提供给suggestion1Stream。另一方面,combineLatest方法是对称的:只要新的结果分发到responseStream,它就会将第一个关闭按钮的最近一次点击事件绑定起来并产生一个新的suggestion。这种对称性很有趣啊,因为它允许我们简化之前的suggestion1Stream代码,就像这样;

1
2
3
4
5
6
7
8
9
10
11
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()使用了最近的两个数据源,但是如果其中一个数据源还没有分发任何值,那么combineLateset方法就不能够在输出流上产生新的数据事件。如果你看过上面的ASCII图表,你会明白当第一个流分发a值的时候并没有任何输出。只有当第二个流分发b值的时候,才会产生输出值。

有几种不同的方式可以解决这个问题,我们采取最简单的方式,在启动的时候为第一个关闭按钮模拟一个点击事件:

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

#包装

所有的细节都处理完后,完整的代码是这样的:

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

你可以从这里看到可以运行的示例:code

代码很少,但含金量很高啊:它的特点是:通过恰当的关注点分离,对多个事件进行管理,甚至是结果的缓存。这个函数的风格让代码看起来更有解释性而不是冗余性:我们没有制定要执行的序列,只是通过定义流之间的关系来表明我们要做什么。比如,通过Rx我们告诉计算机,suggestion1Stream是一个从最新请求的结果绑定了一个user的”close 1”流,除此之外,在刷新发生或者程序启动的时候会被置为null。

同时使我们印象深刻的是,没有像 if ,for while,这样的元素以及JS应用中典型的依赖回调的控制流。如果有必要的话,你甚至可以通过filter函数替代subscribe函数中的if和else。在Rx中,我们拥有像map,filter,scan,merge,combineLatest,startWith这样的流函数,和很多事件驱动编程的的流控制函数。这个工具集会让你用最少的代码拥有更强大的功能。

#接下来
如果你更喜欢把Rx系列作为反应式编程的library,那么你可以花些时间研究这个函数列表:transforming,combining,和Observables的创建。如果你希望通过图表的方式理解它们,你可以看这里RxJava’s very useful documentation with marble diagrams。 当你准备沉下心做些事情的时候,将它们绘画成图表,思考它,然后再看一下长长的函数列表,再思考。这种方式在我的经验中证明是非常高效的。

一旦你开始使用Rx进行编程,就需要完全的理解Cold和Hot Observables的概念。如果你现在忽略它,回头它会让踩不少坑。我可是警告过你喽!通过学习真正的函数式编程,你可以强大自己的技能库,并且了解Rx编程的负面影响。

但是响应式编程不仅仅是Rx。这里提供的Bacon.js 你可以直接使用,省去了Rx中的一些生疏的概念。 Elm Language有它自己的分类:反应式函数式编程,结合了JS+HTML+CSS,并且拥有一个time travelling debugger.相当完美。

对于事件依赖过重的前端和app来说,Rx非常有效。但是并不只针对客户端,在后端以及数据库方面也能处理的很好。事实上,RxJava在Netflix的API中是一个非常重要的处理服务器端并发性的组件。Rx不是一个限制了特定类型应用和语言的框架。对于任何事件驱动的软件编程,它真的是一个可以参考的范例。

希望此教程能够帮到你!