很明显你是有兴趣学习这种被称作响应式编程的新技术才来看这篇文章的。
学习响应式编程是很困难的一个过程,特别是在缺乏优秀资料的前提下。刚开始学习时,我试过去找一些教程,并找到了为数不多的实用教程,但是它们都流于表面,从没有围绕响应式编程构建起一个完整的知识体系。库的文档往往也无法帮助你去了解它的函数。不信的话可以看一下这个:
通过合并元素的指针,将每一个可观察的元素序列放射到一个新的可观察的序列中,然后将多个可观察的序列中的一个转换成一个只从最近的可观察序列中产生值得可观察的序列。
天啊。
我看过两本书,一本只是讲述了一些概念,而另一本则纠结于如何使用响应式编程库。我最终放弃了这种痛苦的学习方式,决定在开发中一边使用响应式编程,一边理解它。在 Wikipedia 一如既往的空泛与理论化。Reactive Manifesto 看起来是你展示给你公司的项目经理或者老板们看的东西。微软的 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
既然已经开始对响应式编程感到熟悉,为了不让你觉得无聊,我们可以尝试做一些新东西:我们将会把一个 Click event stream 转为新的 Click event stream。
首先,让我们做一个能记录一个按钮点击了多少次的计数器 Stream。在常见的响应式编程库中,每个Stream都会有多个方法,如 map
, filter
, scan
, 等等。当你调用其中一个方法时,例如 clickStream.map(f)
,它就会基于原来的 Click stream 返回一个新的 Stream 。它不会对原来的 Click steam 作任何修改。这个特性称为不可变性,它对于响应式编程 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
只是一个简单的添加函数。然后,每 Click 一次, counterStream
就会把点击的总次数发给它的观察者。
为了展示响应式编程真正的实力,让我们假设你想得到一个包含“双击”事件的 Stream。为了让它更加有趣,假设我们想要的这个 Stream 要同时考虑三击(Triple clicks),或者更加宽泛,连击(两次或更多)。深呼吸一下,然后想像一下在传统的命令式且带状态的方式中你会怎么实现。我敢打赌代码会像一堆乱麻,并且会使用一些变量保存状态,同时也有一些计算时间间隔的代码。
而在响应式编程中,这个功能的实现就非常简单。事实上,这逻辑只有 RxJS 作为工具 ,因为JavaScript是现在最多人会的语言,而 .NET, JavaScript, Python,Objective-C/Cocoa, Groovy等等)。所以,无论你用的是什么工具,你都能从下面这个教程中受益。
在 Twitter 上,这个表明其他账户的 UI 元素看起来是这样的:
我们将会重点模拟它的核心功能,如下:
我们可以忽略其它的特性和按钮,因为它们是次要的。同时,因为 Twitter 最近关闭了对非授权用户的 API,我们将会为 Github 实现这个推荐界面,而非 Twitter。这是subscribing 这个 Stream。
requestStream.subscribe(function(requestUrl) {
// execute the request
jQuery.getJSON(requestUrl, function(responseData) {
// ...
});
}
留意一下我们使用了 jQuery 的 Ajax 函数(我们假设你已经知道 Rx.Observable.create()
所做的事就是通过显式的通知每一个 Observer (或者说是“Subscriber”) Data events(onNext()
)或者 Errors ( onError()
)来创建你自己的 Stream。而我们所做的就只是把 jQuery Ajax Promise 包装起来而已。打扰一下,这意味者Promise本质上就是一个Observable?
是的。
Observable 就是 Promise++。在 Rx 中,你可以用 var stream = Rx.Observable.fromPromise(promise)
轻易的把一个 Promise 转为 Observable,所以我们就这样子做吧。唯一的不同就是 Observable 并不遵循pointers:每个映射的值都是一个指向其它 Stream 的指针。在我们的例子里,每个请求 URL 都会被映射一个指向包含响应 Promise stream 的指针。
Response 的 Metastream 看起来会让人困惑,并且看起来也没有帮到我们什么。我们只想要一个简单的响应 stream,其中每个映射的值应该是 JSON 对象,而不是一个 JSON 对象的'Promise'。是时候介绍 (Mr. Flatmap)(merge()
函数。这就是它做的事的图解:
stream A: ---a--------e-----o----->
stream B: -----B---C-----D-------->
vvvvvvvvv merge vvvvvvvvv
---a-B---C--e--D--o----->
这样就简单了:
var requestOnRefreshStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
});
var startupRequestStream = Rx.Observable.just('https://api.github.com/users');
var requestStream = Rx.Observable.merge(
requestOnRefreshStream, startupRequestStream
);
还有一个更加简洁的可选方案,不需要使用中间变量。
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.merge(Rx.Observable.just('https://api.github.com/users'));
甚至可以更简短,更具有可读性:
var requestStream = refreshClickStream
.map(function() {
var randomOffset = Math.floor(Math.random()*500);
return 'https://api.github.com/users?since=' + randomOffset;
})
.startWith('https://api.github.com/users');
所以让我们把显示的推荐设计成一个 stream,其中每一个映射的值都是包含了推荐内容的 JSON 对象。我们以此把三个推荐内容分开来。现在第一个推荐看起来是这样子的:
var suggestion1Stream = responseStream
.map(function(listUsers) {
// get one random user from the list
return listUsers[Math.floor(Math.random()*listUsers.length)];
});
其他的, suggestion2Stream
和 suggestion3Stream
可以简单的拷贝 suggestion1Stream
的代码来使用。这不是 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-->
还有一个功能需要实现。每一个推荐,都该有自己的"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 的响应页面有 100 个用户,而我们仅仅使用其中的三个,所以还有很多的新数据可以使用,无须重新发起请求。
同样的,我们用Stream的方式来思考。当点击'close1'时,我们想要用 responseStream
最近的映射从响应列表中获取一个随机的用户,如:
requestStream: --r--------------->
responseStream: ------R----------->
close1ClickStream: ------------c----->
suggestion1Stream: ------s-----s----->
在 Rx* 中, 叫做连接符函数的 big list of functions,它包括了如何转换、合并、以及创建 Observable。如果你想通过图表去理解这些函数,请看 Cold vs Hot Observables 中的概念。如果忽略了这些,你一不小心就会被它坑了。我提醒过你了。通过学习真正的函数式编程去提升自己的技能,并熟悉那些会影响到 Rx 的问题,比如副作用。
但是响应式编程不仅仅是 Rx。还有相对容易理解的 Elm Language 则以它自己的方式支持 RP:它是一门会编译成 Javascript + HTML + CSS 的响应式编程语言 ,并有一个 RxJava 是实现Netflix's API服务器端并发的一个重要组件 。Rx 并不是一个只能在某种应用或者语言中使用的 Framework。它本质上是一个在开发任何 Event-driven 软件中都能使用的编程范式。
本文来自于:http://wiki.jikexueyuan.com/project/android-weekly/issue-145/introduction-to-RP.html