什么是响应式编程(Reactive Programming)响应式编程介绍

来源:推酷时间:2017-03-10 17:05:48

  什么是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开始。

1.png

  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的最好方法,无论你是初学者还是专家。

2.png

  灰色的方框是用来转换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上,这个界面看起来是这样的:

3.png

  我们将会重点模拟它的核心功能,如下:

  启动时从API那里加载帐户数据,并显示3个推荐

  点击"Refresh"时,加载另外3个推荐用户到这三行中

  点击帐号所在行的'x'按钮时,清除那个推荐然后显示一个新的推荐

  每行都会显示帐号的头像,以及他们主页的链接

  我们可以忽略其它的特性和按钮,因为它们是次要的。同时,因为Twitter最近关闭了对非授权用户的API,我们将会为Github实现这个推荐界面,而非Twitter。这是Github获取用户的API。

  如果你想先看一下最终效果,这里有完成后的代码 。

  Request与Response

  在RP中你该怎么处理这个问题呢? 好吧,首先,(几乎) 所有的东西都可以转为一个Stream 。这就是RP的咒语。让我们先从最简单的特性开始:"在启动时,从API加载3个帐号的数据"。这并没有什么特别,就只是简单的(1)发出一个请求,(2)收到一个响应,(3)渲染这个响应。所以,让我们继续,并用Stream代表我们的请求。一开始可能会觉得杀鸡用牛刀,但我们应当从最基本的开始,是吧?

  在启动的时候,我们只需要发出一个请求,所以如果我们把它转为一个Data stream的话,那就是一个只有一个Value的Stream。稍后,我们知道将会有多个请求发生,但现在,就只有一个请求。

  --a------|->

  a是一个String ';

  复制代码

  这是一个包含了我们想向其发出请求的URL的Stream。每当一个请求事件发生时,它会告诉我们两件事:"什么时候"与"什么东西"。"什么时候"这个请求会被执行,就是什么时候这个Event会被emit。"什么东西"会被请求,就是这个emit出来的Value:一个包含URL的String。

  在RX*中,创建只有一个Value的Stream是非常简单的。官方把一个Stream称作Observable,因为它可以被观察(can be observed => observable),但是我发现那是个很傻逼的名子,所以我把它叫做 Stream 。

  var requestStream = Rx.Observable.returnValue(';);

  复制代码

  但是现在,那只是一个包含了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.

文章内容来源于网络,不代表本站立场,若侵犯到您的权益,可联系我们删除。(本站为非盈利性质网站) 联系邮箱:rjfawu@163.com