指路Reactive Programming
指路Reactive Programming
我在工作中采用Reactive Programming(RP)已经有一年了,对于这个“新鲜”的辞藻或许有一些人还不甚熟悉,这里就和大家说说关于RP我的理解。希望在读完本文后,你能够用Reactive Extension进行RP。
需要说明的是,我实在不知道如何翻译Reactive Programming这个词组,所以在本文中均用RP代替,而不是什么“响应式编程”、“反应式编程”。本文假定你对JavaScript及HTML5有初步的了解,如果有使用过,那么就再好不过了。
让我们首先来想象一个很常见的交互场景。当用户点击一个页面上的按钮,程序开始在后台执行一些工作(例如从网络获取数据)。在获取数据期间,按钮不能再被点击,而会显示成灰色的”disabled”状态。当加载完成后,页面展现数据,而后按钮又可以再次使用。(如下面例子的这个load按钮)
在这里我使用jQuery编写了按钮的逻辑,具体的代码是这样的。
1 |
var loading = false; $('.load').click(function () { |
对应的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 = [ clickEvent, clickEvent, clickEvent, clickEvent, clickEvent, ... ] |
你会想,古怪之处在于,这里没了时间的概念。其实不一定是这样的。你觉得这里少了时间,只是因为你被我刚才的例子所迷惑了。你的脑袋里面可能是在想下面的这段代码:
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 |
text$.subscribe(function (text) { |
这样就用完全Reactive的方式重构了之前我们的例子。
在我们重构后的方案中,消灭了所有的状态。状态都被Observable抽象了出去。于是,这样的代码如果放在一个函数里面,这个函数将是没有副作用的纯函数。关于纯函数、函数式编程,可以阅读我的文章《“函数是一等公民”背后的含义》。
总结
本文从应用的角度入手解释了Reactive Programming的思路。Observable作为对状态的抽象,统一了Iterative和Reactive,淡化了两者之间的边界。当然,最大的好处就是我们用抽象的形式将烦人的状态赶出了视野,取而代之的是可组合的、可变换的Observable。
事物之间的对立统一通常很难找到。实际上,即使是在《设计模式》这本书中,作者们也未曾看到迭代器模式和观察者模式之间存在的对称关系。在UI设计领域,我们更多地和用户驱动、通信驱动出来的事件打交道,这才促成了这两个模式的合并。
指路Reactive Programming的更多相关文章
- .Net中的反应式编程(Reactive Programming)
系列主题:基于消息的软件架构模型演变 一.反应式编程(Reactive Programming) 1.什么是反应式编程:反应式编程(Reactive programming)简称Rx,他是一个使用LI ...
- Unity基于响应式编程(Reactive programming)入门
系列目录 [Unity3D基础]让物体动起来①--基于UGUI的鼠标点击移动 [Unity3D基础]让物体动起来②--UGUI鼠标点击逐帧移动 时光煮雨 Unity3D让物体动起来③—UGUI DoT ...
- ReactiveCocoa与Functional Reactive Programming
转自 http://blog.leezhong.com/ios/2013/06/19/frp-reactivecocoa.html Functional Reactive Programming(以下 ...
- "Principles of Reactive Programming" 之<Actors are Distributed> (1)
week7中的前两节课的标题是”Actors are Distributed",讲了很多Akka Cluster的内容,同时也很难理解. Roland Kuhn并没有讲太多Akka Clus ...
- "reactive programming"的概念
下面的内容大多是翻译来的. Reactive Programming? What is Reactive Programming? 为了了解Reactive——从编程范式至其背后的动机,有必要了解现在 ...
- "Principles of Reactive Programming" 之 <Persistent Actor State>学习笔记
这是<Pinciples of Reactive Programming>week6的最后一课. 为什么需要把actor的状态持久化? 如果actor没有状态,那么在任何实时,这个acto ...
- [Reactive Programming] RxJS dynamic behavior
This lesson helps you think in Reactive programming by explaining why it is a beneficial paradigm fo ...
- [Reactive Programming] Using an event stream of double clicks -- buffer()
See a practical example of reactive programming in JavaScript and the DOM. Learn how to detect doubl ...
- [RxJS] Reactive Programming - What is RxJS?
First thing need to understand is, Reactive programming is dealing with the event stream. Event stre ...
随机推荐
- Dynamics 365-N:N Relationship的记录处理
在Dynamics CRM中,两个Entity之间是可以有N:N Relationship的,但是CRM实现N:N,是通过系统自创建一个中间表来实现的.这个中间表,如果环境是OnPremise,可以根 ...
- C# 通用单例窗体类
/// <summary> /// 通用的单例制作器 /// </summary> /// <typeparam name="T"></t ...
- 简单概括下MongoDB 4.0 新特性
(1)跨文档事务支持 (ACID) 首个支持跨文档事务的NoSQL云数据库,将文档模型的速度,灵活性和功能与ACID保证相结合.现在,使用MongoDB解决各种用例变得更加容易. (2)40%迁移速度 ...
- Window10上安装Redis及其客户端
资源下载地址 Redis安装包:https://github.com/MicrosoftArchive/redis/releases Redis客户端: 链接:https://pan.baidu.co ...
- python学习——读取染色体长度(五:从命令行输入染色体长度)
# 传递命令行参数 # 导入sys模块 import sys print(sys.argv) 命令行操作 python argv.py 10 20 30 40 50 回车输出 ['argv.py' ...
- servlet中 java.lang.ClassNotFoundException: com.mysql.jdbc.Driver异常
解决方法:将mysql-connector-java-xxx-bin.jar包,复制到项目下WebContent/WEB-INF/lib目录下,刷新重启tomcat运行即可.
- animation动画案例
最近一直苦恼做一个banner的进度条,原先用js改变width值,但明显卡顿.后来用了animation,超级好用. <!DOCTYPE html> <html lang=&quo ...
- 隐写术之steghide的使用
steghide不是一个软件,所以下载之后解压缩就可以在命令行中使用. win+R,cmd,回车->进入到steghide.exe所在的文件夹,使用隐藏或者解锁的相应命令,即可隐藏或者解锁. 这 ...
- linux驱动简单介绍
linux驱动简单介绍 驱动基本介绍 驱动.顾名思义就是“驱使硬件设备行动”.设备驱动与底层硬件之间打交道,按照硬件设备的具体操作方式来读写设备寄存器,最终完成一系列操作. 设备 驱动充当了应用程序 ...
- mongoTemplate查询大数据过慢
先上两段代码 代码一 Query query = new Query();queryAfter.addCriteria(Criteria.where("id").in(idList ...