作者: @andrestaltz
翻译:@benjycui、@jsenjoy
作者在原文后回答了不少人的疑惑,推荐一看。
在翻译时,术语我尽量不翻译,就算翻译了也会给出原文以作对照。因为就个人观察的情况而言,术语翻译难以统一,不同的译者会把同一个概念翻译成不同的版本,最终只会让读者困惑。而且术语往往就几个单词,记起来也不难。
作者在回复中提了一下frp与rp是不同的,同时建议把这份教程中的frp都替换为rp,所以译文就把frp都替换为rp了。
很明显你是有兴趣学习这种被称作rp(reactive programming)的新技术才来看这篇文章的。
学习rp是很困难的一个过程,特别是在缺乏优秀资料的前提下。刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它们都流于表面,从没有围绕rp构建起一个完整的知识体系。库的文档往往也无法帮助你去了解它的函数。不信的话可以看一下这个:
rx.observable.prototype.flatmaplatest(selector, [thisarg])
!@#¥%……&*
尼玛。
我看过两本书,一本只是讲述了一些概念,而另一本则纠结于如何使用rp库。我最终放弃了这种痛苦的学习方式,决定在开发中一边使用rp,一边理解它。在 futurice工作期间,我尝试在真实项目中使用rp,并且当我遇到困难时,得到了 同事们的帮助 。
在学习过程中最困难的一部分是 以rp的方式思考 。这意味着要放弃命令式且带状态的(imperative and stateful)编程习惯,并且要强迫你的大脑以一种不同的方式去工作。在互联网上我找不到任何关于这方面的教程,而我觉得这世界需要一份关于怎么以rp的方式思考的实用教程,这样你就有足够的资料去起步。库的文档无法为你的学习提供指引,而我希望这篇文章可以。
什么是rp?
在互联网上有着一大堆糟糕的解释与定义。 维基百科 一如既往的空泛与理论化。stackoverflow 的权威答案明显不适合初学者。 reactive manifesto 看起来是你展示给你公司的项目经理或者老板们看的东西。微软的 rx terminology rx = observables + linq + schedulers 过于重量级且微软味十足,只会让大部分人困惑。相对于你所使用的mv*框架以及钟爱的编程语言,reactive和propagation of change这些术语并没有传达任何有意义的概念。框架的views层当然要对models层作出反应,改变当然会传播(分别对应上文的reactive与propagation of change,意思是这一大堆术语和废话差不多,翻译不好,只能靠备注了)。如果没有这些,就没有东西会被渲染了。
所以不要再扯这些废话了。
rp是使用异步数据流进行编程
一方面,这并不是什么新东西。event buses或者click events本质上就是异步事件流(asynchronous event stream),你可以监听并处理这些事件。rp的思路大概如下:你可以用包括click和hover事件在内的任何东西创建data stream(原文:frp is that idea on steroids. you are able to create data streams of anything, not just from click and hover events.)。stream廉价且常见,任何东西都可以是一个stream:变量、用户输入、属性、cache、数据结构等等。举个例子,想像一下你的twitter feed就像是click events那样的data stream,你可以监听它并相应的作出响应。
在这个基础上,你还有令人惊艳的函数去combine、create、filter这些stream。 这就是函数式(functional)魔法的用武之地。stream能接受一个,甚至多个stream为输入。你可以 merge 两个stream,也可以从一个stream中 filter 出你感兴趣的events以生成一个新的stream,还可以把一个stream中的data values map 到一个新的stream中。
既然stream在rp中如此重要,那么我们就应该好好的了解它们,就从我们熟悉的clicks on a button event stream开始。
stream就是一个 按时间排序的events(ongoing events ordered in time)序列 ,它可以emit三种不同的events:(某种类型的)value、error或者一个completed signal。考虑一下completed发生的时机,例如,当包含这个button(指上面clicks on a button例子中的button)的window或者view被关闭时。
通过分别为value、error、completed定义事件处理函数,我们将会异步地捕获这些events。有时可以忽略error与completed,你只需要定义value的事件处理函数就行。监听一个stream也被称作是 订阅(subscribing) ,而我们所定义的函数就是观察者(observer),stream则是被观察者(observable),其实就是 观察者模式(observer design pattern) 。
上面的示意图也可以使用ascii重画为下图,在下面的部分教程中我们会使用这幅图:
--a---b-c---d---x---|->
a, b, c, d are emitted values
x is an error
| is the 'completed' signal
---> is the timeline
复制代码
既然已经开始对rp感到熟悉,为了不让你觉得无聊,我们可以尝试做一些新东西:我们将会把一个click event stream转为新的click event stream。
首先,让我们做一个能记录一个按钮点击了多少次的计数器stream。在常见的rp库中,每个stream都会有多个方法, map 、 filter 、 scan 等等。当你调用其中一个方法时,例如 clickstream.map(f) ,它就会基于原来的click stream返回一个 新的stream 。它不会对原来的click steam作任何修改。这个特性就是 不可变性(immutability) ,它之于rp stream,就如果汁之于薄煎饼。我们也可以对方法进行链式调用如 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 函数把原stream中的value分别映射到新的stream中。在我们的例子中,我们把每一次click都映射为数字1。 scan(g) 会根据你提供的 g函数把stream中的所有value聚合成一个value -- x = g(accumulated, current),这个示例中 g 只是一个简单的add函数。然后,每click一次, counterstream 就会把点击的总次数发给它的观察者。
为了展示rp真正的实力,让我们假设你想得到一个包含双击事件的stream。为了让它更加有趣,假设我们想要的这个stream要同时考虑三击(triple clicks),或者更加宽泛,连击(multiple clicks)。深呼吸一下,然后想像一下在传统的命令式且带状态的方式中你会怎么实现。我敢打赌代码会像一堆乱麻,并且会使用一些的变量保存状态,同时也有一些计算时间间隔的代码。
而在rp中,这个功能的实现就非常简单。事实上,这逻辑只有 4行代码 。但现在我们先不管那些代码。用图表的方式思考是理解怎样构建stream的最好方法,无论你是初学者还是专家。
灰色的方框是用来转换stream的函数。首先,我们把连续250ms内的click都放进一个列表(原文:first we accumulate clicks in lists, whenever 250 milliseconds of event silence has happened. 实在不知道怎么翻译,就按自己的理解写了一下) -- 简单来说就是 buffer(stream.throttle(250ms)) 做的事,不要在意这些细节,我们只是展示一下rp而已。结果是一个列表的stream,然后我们使用 map() 把每个列表映射为一个整数,即它的长度。最终,我们使用 filter(x >= 2) 把整数 给过滤掉。就这样,3个操作就生成了我们想要的stream。然后我们就可以订阅(监听)这个stream,并以我们所希望的方式作出反应。
我希望你能感受到这个示例的优美之处。这个示例只是冰山一角:你可以把同样的操作应用到不同种类的stream上,例如,一个api响应的stream;另一方面,还有很多其它可用的函数。
为什么我要使用rp
rp提高了代码的抽象层级,所以你可以只关注定义了业务逻辑的那些相互依赖的事件,而非纠缠于大量的实现细节。rp的代码往往会更加简明。
特别是在开发现在这些有着大量与data events相关的ui events的高互动性webapps、mobile apps的时候,rp的优势将更加明显。10年前,网页的交互就只是提交一个很长的表单到后端,而前端只有简单的渲染。apps就表现得更加的实时了:修改一个表单域就能自动地把修改后的值保存到后端,为一些内容点赞时,会实时的反应到其它在线用户那里等等。
现在的apps有着大量各种各样的实时events,以给用户提供一个交互性较高的体验。我们需要工具去应对这个变化,而rp就是一个答案。
以rp方式思考的例子
让我们做一些实践。一个真实的例子一步一步的指导我们以rp的方式思考。不是虚构的例子,也没有只解释了一半的概念。学完教程之后,我们将写出真实可用的代码,并做到知其然,知其所以然。
在这个教程中,我将会使用 javascript 和 rxjs ,因为javascript是现在最多人会的语言,而 rx* 库 有多种语言版本,并支持多种平台( .net,java,scala,clojure,javascript,ruby,python,c++,objective-c/cocoa,groovy, 等等)。所以,无论你用的是什么语言、库,你都能从下面这个教程中学到东西。
实现who to follow推荐界面
在twitter上,这个界面看起来是这样的:
我们将会重点模拟它的核心功能,如下:
启动时从api那里加载帐户数据,并显示3个推荐
点击refresh时,加载另外3个推荐用户到这三行中
点击帐号所在行的'x'按钮时,清除那个推荐然后显示一个新的推荐
每行都会显示帐号的头像,以及他们主页的链接
我们可以忽略其它的特性和按钮,因为它们是次要的。同时,因为twitter最近关闭了对非授权用户的api,我们将会为github实现这个推荐界面,而非twitter。这是github获取用户的api。
如果你想先看一下最终效果,这里有完成后的代码http://jsfiddle.net/staltz/8jfjh/48/ 。
request与response
在rp中你该怎么处理这个问题呢? 好吧,首先,(几乎) 所有的东西都可以转为一个stream 。这就是rp的咒语。让我们先从最简单的特性开始:在启动时,从api加载3个帐号的数据。这并没有什么特别,就只是简单的(1)发出一个请求,(2)收到一个响应,(3)渲染这个响应。所以,让我们继续,并用stream代表我们的请求。一开始可能会觉得杀鸡用牛刀,但我们应当从最基本的开始,是吧?
在启动的时候,我们只需要发出一个请求,所以如果我们把它转为一个data stream的话,那就是一个只有一个value的stream。稍后,我们知道将会有多个请求发生,但现在,就只有一个请求。
--a------|->
a是一个string 'https://api.github.com/users'
复制代码
这是一个包含了我们想向其发出请求的url的stream。每当一个请求事件发生时,它会告诉我们两件事:什么时候与什么东西。什么时候这个请求会被执行,就是什么时候这个event会被emit。什么东西会被请求,就是这个emit出来的value:一个包含url的string。
在rx*中,创建只有一个value的stream是非常简单的。官方把一个stream称作observable,因为它可以被观察(can be observed => observable),但是我发现那是个很傻逼的名子,所以我把它叫做 stream 。
var requeststream = rx.observable.returnvalue('https://api.github.com/users');
复制代码
但是现在,那只是一个包含了string的stream,并没有什么特别,所以我们需要以某种方式使value被emit。就是通过订阅(subscribing)这个stream。
requeststream.subscribe(function(requesturl) {
// execute the request
jquery.getjson(requesturl, function(responsedata) {
// ...
});
}
复制代码
留意一下我们使用了jquery的ajax函数(我们假设你已经知道 它的用途 )去发出异步请求。但先等等,rp可以用来处理 异步 data stream。那这个请求的响应就不能当作一个包含了将会到达的数据的stream么?当然,从理论上来讲,应该是可以的,所以我们尝试一下。
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() 所做的事就是通过显式的通知每一个observer(或者说是subscriber) data events( onnext() )或者errors ( onerror() )来创建你自己的stream。而我们所做的就只是把jquery ajax promise包装起来而已。 打扰一下,这意味者promise本质上就是一个observable?
yes.
observable就是promise++。在rx中,你可以用 var stream = rx.observable.frompromise(promise) 轻易的把一个promise转为observable,所以我们就这样子做吧。唯一的不同就是observable并不遵循 promises/a+ ,但概念上没有冲突。promise就是只有一个value的observable。rp stream比promise更进一步的是允许返回多个value。
这样非常不错,并展现了rp至少有promise那么强大。所以如果你相信promise宣传的那些东西,那么也请留意一下rp能胜任些什么。
现在回到我们的例子,如果你已经注意到了我们在 subscribe() 内又调用了另外一个 subscribe() ,这类似于callback hell。同样,你应该也注意到responsestream 是建立在 requeststream 之上的。就像你之前了解到的那样,在rp内有简单的机制可以从其它stream中转换并创建出新的stream,所以我们也应该这样子做。
你现在需要知道的一个基本的函数是 map(f) ,它分别把 f() 应用到stream a中的每一个value,并把返回的value放进stream b里。如果我们也对request stream与response stream进行同样的处理,我们可以把request url映射(map)为response promise(而promise可以转为streams)。
var responsemetastream = requeststream
.map(function(requesturl) {
return rx.observable.frompromise(jquery.getjson(requesturl));
});
复制代码
然后,我们将会创造一个叫做 metastream 的怪物:包含stream的stream。暂时不需要害怕。metastream就是emit的每个value都是stream的stream。你可以把它想像为 指针(pointer) :每个value都是一个指向其它stream的指针。在我们的例子里,每个request url都会被映射(map)为一个指向包含响应promise stream的指针。
response的metastream看起来会让人困惑,并且看起来也没有帮到我们什么。我们只想要一个简单的response stream,它返回的value应该是json而不是一个json对象的'promise'。是时候介绍mr. flatmap了:它是 map() 的一个版本,通过把应用到trunk stream上的所有操作都应用到branch stream上,可以flatten metastream。flatmap并不是用来fix metastream的,因为metastream也不是一个bug,这只是一些用来处理rp中的异步响应(asynchronous response)的工具。
var responsestream = requeststream
.flatmap(function(requesturl) {
return rx.observable.frompromise(jquery.getjson(requesturl));
});
复制代码
很好。因为response stream是根据request stream定义的,所以 如果 我们后面在request stream上发起更多的请求的话,在response stream上我们将会得到相应的response event,就像预期的那样:
requeststream: --a-----b--c------------|->
responsestream: -----a--------b-----c---|->
(小写字母是一个request,大写字母是对应的response)
复制代码
现在,我们终于有了一个response stream,所以可以把收到的数据渲染出来了:
responsestream.subscribe(function(response) {
// render `response` to the dom however you wish
});
复制代码
把目前为止所有的代码放到一起就是这样:
var requeststream = rx.observable.returnvalue('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
});
复制代码
refresh按钮
我之前并没有提到返回的json是一个有着100个用户数据的列表。因为这个api只允许我们设置偏移量(offset),而无法设置返回的用户数,所以我们现在是只用了3个用户的数据而浪费了另外97个的数据。这个问题暂时可以忽略,稍后我们会学习怎么缓存这些数据。
每点击一次refresh按钮,request stream就会emit一个新的url,同时也会返回一个新的response。我们需要两样东西:一个是refresh按钮上click events组成的stream(咒语:一切皆stream),而request stream将改为随refresh click stream作出反应。幸运的是,rxjs提供了从event listener生成observable的函数。
var refreshbutton = document.queryselector('.refresh');
var refreshclickstream = rx.observable.fromevent(refreshbutton, 'click');
复制代码
既然refresh click event本身并没有提供任何要请求的api url,我们需要把每一次的click都映射为一个url。现在,我们把refresh click stream映射为新的request stream,其中每一个click都分别映射为对api请求一个随机偏移量的url。
var requeststream = refreshclickstream
.map(function() {
var randomoffset = math.floor(math.random()*500);
return 'https://api.github.com/users?since=' + randomoffset;
});
复制代码
因为我比较笨并且也没有使用自动化测试,所以我刚把之前做好的一个特性搞烂了。现在在启动时不会再发出任何的request,而只有在点击refresh按钮时才会。额...这两个行为我都需要:无论是点击refresh按钮时还是刚打开页面时都该发出一个request。
我们知道怎么分别为这两种情况生成stream:
var requestonrefreshstream = refreshclickstream
.map(function() {
var randomoffset = math.floor(math.random()*500);
return 'https://api.github.com/users?since=' + randomoffset;
});
var startuprequeststream = rx.observable.returnvalue('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.returnvalue('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.returnvalue('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() 函数做的事和你预期的完全一样。无论你输入的stream是怎样,startwith(x) 输出的stream一开始都是 x 。但是还不够 dry ,我重复了api url。一个改进的方法是移掉 refreshclickstream 最后的 startwith() ,并在一开始的时候emulate一次click。
var requeststream = refreshclickstream.startwith('startup click')
.map(function() {
var randomoffset = math.floor(math.random()*500);
return 'https://api.github.com/users?since=' + randomoffset;
});
复制代码
很好。如果你把之前我搞烂了的版本的代码和现在的相比,就会发现唯一的不同是加了 startwith() 函数。
用stream构建三个推荐
到现在为止,我们只是谈及了这个 推荐 ui元素在responestream的 subscribe()内执行的渲染步骤。对于refresh按钮,我们还有一个问题:当你点击 refresh 时,当前存在的三个推荐并不会被清除。新的推荐会在response到达后出现,为了让ui看起来舒服一些,当点击刷新时,我们需要清理掉当前的推荐。
refreshclickstream.subscribe(function() {
// clear the 3 suggestion dom elements
});
复制代码
不,别那么快,朋友。这样不好,我们现在有 两个 subscriber会影响到推荐的dom元素(另外一个是 responsestream.subscribe() ),而且这样完全不符合separation of concerns 。还记得rp的咒语么?
所以让我们把显示的推荐设计成emit的值为一个包含了推荐内容的json对象的stream。我们以此把三个推荐内容分开来。现在第一个推荐看起来是这样子的:
var suggestion1stream = responsestream
.map(function(listusers) {
// get one random user from the list
return listusers[math.floor(math.random()*listusers.length)];
});
复制代码
其他的, suggestion2stream 和 suggestion3stream 可以简单的拷贝suggestion·stream 的代码来使用。这不是dry,它会让我们的例子变得更加简单一些,加之我觉得这是一个可以帮助考虑如何减少重复的良好实践。
我们不在responsestream的subscribe()中处理渲染了,我们这么处理:
suggestion1stream.subscribe(function(suggestion) {
// render the 1st suggestion to the dom
});
复制代码
回到当刷新时,清理掉当前的推荐,我们可以很简单的把刷新点击映射为 null(即没有推荐数据),并且在 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) 添加到suggestion stream就完成了:
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-->
复制代码
关闭推荐并缓存response
还有一个功能需要实现。每一个推荐,都该有自己的x按钮以关闭它,然后在该位置加载另一个推荐。最初的想法,是点击任何关闭按钮时都需要发起一个新的请求:
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的response有100个用户,而我们仅仅使用其中的三个,所以还有很多的新数据可以使用,无须重新发起请求。
同样的,我们用stream的方式来思考。当点击'close1'时,我们想要从responsestream 最近emit的 response列表中获取一个随机的用户,如:
requeststream: --r--------------->
responsestream: ------r----------->
close1clickstream: ------------c----->
suggestion1stream: ------s-----s----->
复制代码
在rx*中, combinelatest 似乎实现了我们想要的功能。它接受两个stream,a和b作为输入,当其中一个stream emit一个值时, combinelatest 把最近两个emit的值 a 和 b 从各自的stream中取出并且返回一个 c = f(x,y) , f 为你定义的函数。用图来表示更好:
stream a: --a-----------e--------i-------->
stream b: -----b----c--------d-------q---->
vvvvvvvv combinelatest(f) vvvvvvv
----ab---ac--ec---ed--id--iq---->
f是把值转化成大写字母的函数
复制代码
我们可以在 close1clickstream 和 responsestream 上使用combinelatest(),所以无论什么时候当一个按钮被点击时,我们可以拿到response最新emit的值,并且在 suggestion1stream 上产生一个新的值。另一方面,combinelatest()是对称的,当一个新的response 在 responsestream emit时,它将会把最后的'关闭 1'的点击事件一起合并来产生一个新的推荐。这是有趣的,因为它允许我们把之前的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()无法在output stream中产生一个data event。从上边的ascii图中,你可以看到,在第一个stream emit a 这个值时并没有任何输出产生,只有当第二个stream emit b 时才有值输出。
有多种方法可以解决这个问题,我们选择最简单的一种,一开始在'close 1'按钮上模拟一个点击事件:
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);
复制代码
终于完成了,所有的代码合在一起是这样子:
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之间的关系 定义这是什么 。举个例子,我们使用rp告诉计算机 suggestion1stream 是由 'close 1' stream与最新响应中的一个用户合并(combine)而来,在程序刚运行或者刷新时则是 null 。
留意一下代码中并没有出现如 if 、 for 、 while 这样的控制语句,或者一般javascript应用中典型的基于回调的控制流。如果你想使用 filter() ,上面的subscribe() 中甚至可以不用 if 、 else (实现细节留给读者作为练习)。在rp中,我们有着像 map 、 filter 、 scan 、 merge 、 combinelatest 、startwith 这样的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的rp 语言 ,并有一个 time travelling debugger 。非常nb。
rp在需要处理大量事件的frontend和apps中非常有用。但它不仅仅能用在客户端,在backend或者与database交互时也非常有用。事实上, rxjava是实现netflix's api服务器端并发的一个重要组件 。rp并不是一个只能在某种应用或者语言中使用的framework。它本质上是一个在开发任何event-driven软件中都能使用的编程范式(paradigm)。
原文链接: https://github.com/benjycui/introrx-chinese-edition