姚翔的部落格

响应式编程入门(上)

| Comments

Reactive Programming(响应式编程,以下简称RP)这个概念最近正火,本人也一直对其非常感兴趣,终于得了些空余时间可以学习下这门新技术,于是顺便整理些相关内容,希望可以帮助到一些想入门的朋友们。

我选择入门的框架是RxSwift,在过了一遍它官方提供的Tutorial playground代码后,发觉对RP的理念仍是一头雾水,一番Google后找到了这篇优秀的文章:The introduction to Reactive Programming you’ve been missing。读完之后醍醐灌顶,所以以下的博文内容我希望通过意译这篇文章结合RxSwift里的注册Sample来阐述RP的“流之世界观”(这个名称是我自己起的,因为一旦步入Reactive后,以往解决问题的思路将完全改变)。

RP的学习曲线是蛮高的,如果缺少好的资源的话就更难了。看library的文档往往使人一头雾水,而一些教程文章往往只是教你如何用相关的库,很少触及到其真正架构的思想,从而使人不得要领。学习RP最难的部分是:如何用Reactive的方式思考。它要求我们摒除掉以往编程时的那种老思路(状态化、命令式),用一种完成不同的视角去考虑问题。那么到底什么是响应式编程(Reactive Programming)?

响应式编程就是面向异步数据流的编程

这并不是什么新东西,事件总线或者典型的点击事件们就是一个异步事件流,你可以监听这个流,同时做一些额外的处理业务。响应式无法就是把这个理念给发扬光大了:不光是点击或者悬停事件,你几乎可以给所有东西都创建数据流。流(Stream)是廉价的、无所不在的,任何东西都可以成为流:变量,用户的输入,属性,缓存,数据结构等等。举得例子:想象一个你的微信朋友圈动态就是同点击事件一样的一个数据流,你可以监听它同时对其进行对应的响应。

在这基础之上,你还拥有一套不可思议工具集去操作这些流:合并、创建、过滤等等。这就是所谓的“函数式(functional)”施展魔力的地方。一个甚至多个流可以被当做另一个流的输入;你可以合并两个流;你可以过滤一个流从而得到一个只包含你感兴趣事件的流;你还可以把一个流中的值映射成其他值从而形成一个新的流。

那让我们来仔细看一下“流”,拿我们熟悉的“点击按钮事件”流来举例:

File Inspector

一个流就是一个按照时间排序的持续事件的序列,它会发出三种不同类型的东西:一个某种类型的值(Value)、一个错误(Error)、以及一个完成(Completed)的信号。比方说这个按钮所在的view或者window被关闭了,那么就会发出一个完成信号。我们可以定义不同的函数,使流分别在发出一个值、或者错误、或者完成信号时,执行不同的函数,从而异步地捕获这些发出的事件。有时候我们可以忽略错误和完成事件,只关心产生值的事件。而这个监听行为,我们称之为“订阅(subscribing)”,那些我们定义的函数,就称为“观察者(Observer)”,那些流则称为“对象(subject)”或者“可被观者物(observable)”。

为了方便绘制,我们可以用ASCII来重绘上图:

1
2
3
4
5
6
--a---b-c---d---X---|->

a, b, c, d 是发出的值(Value)
X 是一个错误(Error)
| 是一个完成(completed)信号
---> 表示的是时间轴

接下来让我们玩点有意思的,我们已经有了一个点击事件的流,现在让我们来建立一个记录点击次数的流。在通用的Reactive库中,每个流都提供了很多有用的方法来使用,比如map、filer、scan等等。当你调用其中一个方法时,比如clickStream.map(f),它会返回一个基于点击事件流的新流,它并不会对原来的流做任何改动。这是一个叫做不可变(immutability)的特性,它和Reactive中的流是比目连枝的。这就使得我们可以通过链式调用来生成新的流,比如:clickStream.map(f).scan(g)

1
2
3
4
5
6
7
8
9
  clickStream: ---c----c--c----c------c-->

                  v  map(c becomes 1) v
  
               ---1----1--1----1------1-->
  
                  v      scan(+)      v

counterStream: ---1----2--3----4------5-->

其中map(f)会根据你提供的f函数把输入流中的每个值转化到新的流中去,在上面的例子里,我们将每个点击映射成了数字1。scan(g)则是把所有早先的值聚合成一个新值的函数,新值的计算公式是:x = g(accumulated, current),当前的例子中,g就是一个简单的add(+)函数。就这样,counterStream变成了一个在每次点击时,都会产生一个点击总数的流。

现在让我们来看下Reactive的真正魔力,比方说我们现在需要一个“双击事件”的流,为了提升点难度,我们再假设“三击事件”也需要同双击一样被监听,或者更狠一点,任意的“多次连击事件”都需要被监听。好,冷静,先让我们来想象一下如果用传统的实现方式你会怎么做?我敢打赌那样的代码一定会恶心到自己,需要定义一些变量来记录状态,同时还要对时间间隔做一些必要的处理。

但是,在Reactive的世界里,这是相当简单的事情。实际上只需要4行代码就可以搞定了,不过先让我们撇开代码,无论你是一个菜鸟还是专家,用时间流图的方式去思考是理解如何创建流的最佳方式:

File Inspector

上图灰框里是把一个流转化成另一个流的函数。第一个函数的作用就是把流中的点击事件按照最大250毫秒间隔累计起来成为一个列表流(不要担心不懂这些函数的功能,目前我们只是在演示响应式的思想)。然后我们再通过map()把结果映射成每个列表中元素数量的流,这样我们就获得了连击次数了。最后我们用一个过滤函数过滤了连击数是1的值,这样我们就订阅(subscribe)/监听(listen)这个结果流来实现我们想要的功能了。

怎么样,这样的解决思路是不是很优美?这个例子只是响应式编程的冰山一角,你可以在任何流上都应用这样的操作,比如说一个API请求相应的流,另一方面,这样的函数有非常之多。

那我们为什么要用响应式编程呢?

RP提升了你代码的抽象级别,所以你只需要专注于那些业务逻辑中相互关联的事件,而不需要一直纠结在所有的实现细节中。RP中的代码会更简明。

在现代那些有着大量UI交互元素,同时每个交互又有其相关联的数据事件的webapp和手机应用中,RP的益处就更大了。10年前,一个网页的交互可能只是简单的提交一份表单给后端,然后在前端展现一些数据;但如今,应用已经进化成更实时的体验:修改表单中某一项的值可能就自动触发了一个后台保存的事件,“给别人点赞”这样一个操作会实时地反馈到被点赞的用户那边,这样的用例非常之多。所以我们需要一些工具来处理这种实时性的需求,响应式编程就是目前最佳的选择。

(待续……)

Comments