指路Reactive Programming
我在工作中采用Reactive Programming(RP)已经有一年了,对于这个“新鲜”的辞藻或许有一些人还不甚熟悉,这里就和大家说说关于RP我的理解。希望在读完本文后,你能够用Reactive Extension进行RP。
需要说明的是,我实在不知道如何翻译Reactive Programming这个词组,所以在本文中均用RP代替,而不是什么“响应式编程”、“反应式编程”。本文假定你对JavaScript及HTML5有初步的了解,如果有使用过,那么就再好不过了。
让我们首先来想象一个很常见的交互场景。当用户点击一个页面上的按钮,程序开始在后台执行一些工作(例如从网络获取数据)。在获取数据期间,按钮不能再被点击,而会显示成灰色的”disabled”状态。当加载完成后,页面展现数据,而后按钮又可以再次使用。(如下面例子的这个load按钮)
在这里我使用jQuery编写了按钮的逻辑,具体的代码是这样的。
1
|
var loading = false;
|
对应的HTML:
1
|
<button class="load">Load</button>
|
不知道你有没有注意到,在这里loading
变量其实是完全可以不用存在的。而我写出loading
变量,就是为了抓住你的眼球。loading
代表的是一个状态,意思是“我的程序现在有没有在后台加载程序”。
另外还有几个不是很明显的状态。比如按钮的disabled
状态(由$btn.prop('disabled')
获得),以及按钮的文字。在加载的时候,也就是loading === true
的时候,按钮的disable
状态会是true
,而文字会是Loading ...
;在不加载的时候,loading === false
成立,按钮的disabled
状态就应该为false
,而文字就是Load
。
现在让我们用静态的图来描述用户点击一次按钮的过程。

如果用户点击很多次的按钮的话,那么loading
的值的变化将是这样的。
1
|
loading: false -> true -> false -> true -> false -> true -> ...
|
类似像loading
这样的状态(state)在应用程序中随处可见,而且其值的变化可以不局限于两个值。举个栗子,假如我们现在设计微博的前端,一条微博的JSON数据形式如下:
1
|
var aWeibo = {
|
另外有一个weiboList
数组,存储当前用户所看到的微博。
1
|
var weiboList = [
|
这当然是个极度精简的模型了,真实的微博应用一定比这个复杂许多。但是有一个和loading
状态很类似的就是weiboList
,因为我们都知道每过一段时间微博就会自动刷新,也就是说weiboList
也在一直经历着变化。
1
|
weiboList: [一些微博] -> [旧的微博,和一些新的微博] -> [更多的微博] -> ...
|
再次强调,无论是weiboList
还是loading
,它们都是应用程序的状态。上面的用箭头组成的示意图仅仅是我们对状态变化的一种展现形式(或者说建模)。然而,我们其实还可以用更加简单的模型来表现它,而这个模型我们都熟悉 —— 数组。
如果它们都只是数组
如果说loading
变化的过程就是一个数组,那么不妨把它写作:
1
|
var loadingProcess = [false, true, false, true, false, ...]
|
为了表现出这是一个过程,我们将其重新命名为loadingProcess
。不过它没有什么不同,它是一个数组。而且我们还可以注意到,按钮的disabled
状态的变化过程和loadingProcess
的变化过程是一模一样的。我们将disabled
的变化过程命名为disabledProcess
。
1
|
var disabledProcess = [false, true, false, true, false, ...]
|
那么如果将loadingProcess
做下面的处理,我们将得到什么呢?
1
|
var textProcess = loadingProcess.map(function(loading) {
|
我们得到的将是按钮上文字的状态变化过程,也就是$btn.text()
的值。我们将其命名为textProcess
。在有了textProcess
和disabledProcess
之后,就可以直接对UI进行更新。在这里,我们不再需要使用到loadingProcess
了。
1
|
disabledProcess.forEach(function (disabled) {
|
这个变换的过程看起来就像下图。

在YY了那么久之后,你可能会说,不对啊!状态的变化是一段时间内发生的事情,在程序一开始怎么可能就知道之后的全部状态,并全部放到一个数组里面呢?是的,我们在之前刻意省略掉了一个重要的元素,也就是时间(time)。
时间都去哪儿啦?
loadingProcess
是如何得出的?当用户触发按钮的点击事件的时候,loadingProcess
会被置为false
;而当HTTP请求完成的时候,我们将其置为true
。在这里,用户触发点击事件,和HTTP请求完成都是一个需要时间的过程。用户的两次点击之间必定要有时间,就像这样:
clickEvent … clickEvent …… clickEvent ….. clickEvent
两个clickEvent之间一个点我们假设代表一秒钟,用户点击的事件之间是由长度不同的时间间隔开的。
如果我们再尝试用刚才的方法,把click事件表示成一个数组,就会觉得特别的古怪:
1
|
var clickEventProcess =
|
你会想,古怪之处在于,这里没了时间的概念。其实不一定是这样的。你觉得这里少了时间,只是因为你被我刚才的例子所迷惑了。你的脑袋里面可能是在想下面的这段代码:
1
|
// 代码A
|
如果是下面这段代码,我相信你再熟悉不过了,你还会觉得奇怪吗?
1
|
// 代码B
|
代码A中,我们所看到的是迭代器模式(Iterative Pattern)。所谓迭代器模式是对遍历一个集合的算法所进行的抽象。对于一个数组、一个二叉树和一个链表的遍历算法各不相同,但我都可以用统一的一个接口来获取遍历的结果。forEach
就是一个例子。
1
|
数组.forEach(function (元素) { /* ... */});
|
虽然每个forEach
的实现方式一定不同,但是只要接口(即forEach
这个名字以及元素
这个参数)一致,我就可以遍历它们之中任何的一个,不管是数组、二叉树还是二郎神。只要它们都是实现了forEach
的集合。
下面这句话希望你仔细品味:
迭代器模式的一个最大的特点就是,数据是由你向集合索要过来的。
在使用迭代器的时候,我们其实就是在向集合要数据,而且每次都企图一次性要完。
1
|
[1,2,3,4,5].forEach(function (num) {
|
这就好像在对集合说,你把那五个数字给我吧,快点儿,一个接一个一次性给完。在生活中,就好像蛋糕店的服务员帮你切蛋糕一样。你总是在和服务员说,麻烦你再给我下一块,再给我下一块……

而代码B是截然相反的。在代码B中,我们是在等待着数据被推送过来。又拿切蛋糕为例,这次就好像是你一言不发,而服务员一直跟你说,“这块切好了,给你!”。

如果你对设计模式熟悉的话,你应该知道代码B的模式叫做观察者模式(Observer Pattern)。所谓观察者模式,就是你观察集合,当集合告诉你它有元素要给你的时候,你就可以拿到元素。addEventListener
本身就是一个很好的观察者模式的例子。
在切蛋糕的例子中,当你双目注视的服务员,耳朵竖得高高的,你就是在对服务员进行观察。每当服务员告诉你,有一块新的蛋糕切好了,你就过去拿。
迭代器和观察者的对立和统一
迭代器模式和观察者模式本质上是对称的。它们相同的地方在于:
- 都是对集合的遍历(都是那块大蛋糕)
- 每次都只获得一个元素
他们完全相反的地方只有一个:迭代器模式是你主动去要数据,而观察者模式是数据的提供方(切蛋糕的服务员)把数据推给你。他们其实完全可以用同样的接口来实现,例如前面的例子中的代码A,我们来回顾一下:
1
|
// 代码A
|
对于代码B,我们可以进行如下的改写
1
|
// 代码B
|
我们解读一下修改过的代码B。
clickEventProcess.forEach
: 它接受一个回调函数作为参数,并存储在this._fn
里面。这是为了将来在clickEventProcess.onNext
里面调用- 当clickEvent触发的时候,调用
clickEventProcess.onNext(clickEvent)
,将clickEvent
传给了clickEventProcess
clickEventProcess.onNext
将clickEvent
传给了this._fn
,也就是之前我们所存储的回调函数- 回调函数正确地接收到新的点击事件
来看看现在发生了什么……迭代器模式和观察者模式用了同样的接口(API)实现了!因为,它们本质上就是对称的,能用同样的API将两件原本对称的事物给统一起来,这是可以做到的。
迭代器模式,英文叫做Iterative,由你去迭代数据;而观察者模式,要求你对数据来源的事件做出反应(react),所以其实也可以称作是Reactive(能做出反应的)。Iterative和Reactive,互相对称,相爱不相杀。
话外音:在这里我没有明确提及,实际上在观察者模式中数据就是以流(stream)的形式出现。而所谓数组,不过就是无需等待,马上就可以获得所有元素的流而已。从流的角度来理解Iterative和Reactive的对称性也可以,这里我们不多加阐述。
Reactive Extension
上面代码B中我们最后获得了一个新的clickEventProcess
,它不是一个真正意义上的集合,却被我们抽象成了一个集合,一个被时间所间隔开的集合。 Rx.js,也称作Reactive Extension提供给了抽象出这样集合的能力,它把这种集合命名为Observable
(可观察的)。
添加Rx.js及其插件Rx-DOM.js。我们需要Rx-DOM.js,因为它提供网络通讯相关的Observable抽象,稍后我们就会看到。
1
|
<script src="https://cdn.rawgit.com/Reactive-Extensions/RxJS/master/dist/rx.all.min.js"></script>
|
只需要很简单的一句工厂函数(factory method)就可以将鼠标点击的事件抽象成一个Observable
。Rx.js提供一个全局对象Rx
,Rx.Observable
就是Observable的类。
1
|
var loadButton = document.querySelector('.load');
|
click$
就是前面的clickEventProcess
,在这里我们将所有的Observable变量名结尾都添加$
。点击事件是像下面这样子的:
1
|
[click ... click ........ click .. click ..... click ..........]
|
每个点击事件后应该发起一个网络请求。
1
|
var response$$ = click$.map(function () {
|
Rx.DOM.ajax.get
会发起HTTP GET请求,并返回响应(Response)的Observable。因为每次请求只会有一个响应,所以响应的Observable实际上只会有一个元素。它将会是这样的:
1
|
[...[.....response].......[........response]......[....response]...........[....response]......[....response]]
|
由于这是Observable的Observable,就好像二维数组一样,所以在变量名末尾是$$
。 若将click$和response$$的对应关系勾勒出来,会更加清晰。

然而,我们更希望的是直接获得Response的Observble,而不是Response的Observble的Observble。Rx.js提供了.flatMap
方法,可以将二维的Observable“摊平”成一维。你可以参考underscore.js里面的flatten
方法,只不过它是将普通数组摊平,而非将Observable摊平。
1
|
var response$ = click$.flatMap(function () {
|
图示:

对于每一个click事件,我们都想将loading
置为true
;而对于每次HTTP请求返回,则置为false
。于是,我们可以将click$
映射成一个纯粹的只含有true
的Observable,但其每个true
到达的事件都和点击事件到达的时间一样;对于response$
,同样,将其映射呈只含有false
的Observable。最后,我们将两个Observable结合在一起(用Rx.Observable.merge
),最终就可以形成loading$
,也就是刚才我们的loadingProcess
。
此外,$loading
还应有一个初始值,可以用startWith
方法来指定。
1
|
var loading$ = Rx.Observable.merge(
|
整个结合的过程如图所示

有了loading$
之后,我们很快就能得出刚才我们所想要的textProcess
和enabledProcess
。enabledProcess
和loading$
是一致的,就无需再生成,只要生成textProcess
即可(命名为text$
)。
1
|
var text$ = loading$.map(function (loading) {
|
在Rx.js中没有forEach
方法,但有一个更好名字的方法,和forEach
效用一样,叫做subscribe
。这样我们就可以更新按钮的样式了。
1
|
|
这样就用完全Reactive的方式重构了之前我们的例子。
在我们重构后的方案中,消灭了所有的状态。状态都被Observable抽象了出去。于是,这样的代码如果放在一个函数里面,这个函数将是没有副作用的纯函数。关于纯函数、函数式编程,可以阅读我的文章《“函数是一等公民”背后的含义》。
总结
本文从应用的角度入手解释了Reactive Programming的思路。Observable作为对状态的抽象,统一了Iterative和Reactive,淡化了两者之间的边界。当然,最大的好处就是我们用抽象的形式将烦人的状态赶出了视野,取而代之的是可组合的、可变换的Observable。
事物之间的对立统一通常很难找到。实际上,即使是在《设计模式》这本书中,作者们也未曾看到迭代器模式和观察者模式之间存在的对称关系。在UI设计领域,我们更多地和用户驱动、通信驱动出来的事件打交道,这才促成了这两个模式的合并。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2018-03-08 我们如何用Go来处理每分钟100万复杂请求的场景
2018-03-08 Docker系列教程05 容器常用命令
2013-03-08 Ubuntu 用户安装文件较器meld使用,以及添加进右键菜单
2013-03-08 linux c 查看其它程序是否启动。没有则启动他
2013-03-08 linux下C程序:运行单个实例