首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >谈谈FRP和Observable(一)

谈谈FRP和Observable(一)

作者头像
tyrchen
发布2018-03-28 15:44:15
9820
发布2018-03-28 15:44:15
举报
文章被收录于专栏:程序人生程序人生

Observable是方兴未艾的FRP(Functional Reactive Programming)革命里最引人注目的一把火炬。FRP发展了也有两年多了,至今为止,还没有一个很好的定义,wikipedia上的定义和reactivemanifesto.org上的说辞要么太抽象,要么太泛泛。我比较喜欢如下这样一个定义:

FRP is about "datatypes that represent a value 'over time'"

因为它点出了最关键的要素:时间。在FRP出现之前,几乎没有一种软件思想认真考虑过时间这个纬度,即便考虑,也是把时间单独处理,就像爱因斯坦之前的物理学割裂时空一样。在旧有的观念里,变量随着时间的流逝,因着事件的触发虽然不断变化,但它依旧是时空轴上的一个点(一维),而非一条线(二维)。

Elm(一门脱胎于haskell的compile-to-javascript的FRP语言)和ReactiveExtensions(微软对FRP的总结)尝试着改变这一认知。Elm提出了Signal的概念,很形象,可以理解为一个和时间相关的序列。

你可以在Signal上做任何的computation(map/reduce/fold/merge/…),但要保证输出依旧是一个Signal。

有了Signal的概念,变量不再是一个个一维的,离散的数据,而是随着时间一路延展下去的一个流(stream)。此外,函数式编程让人伤神的immutable特性在Signal的概念下很好地和我们熟知的程序世界统一起来:在这个流里,每个单个的值在产生的那一刻就固定下来(immutable),但整个流是不断变化的(是不是有种电磁学和光学统一的既视感?)。一个变量,其状态在t0是a,t1是b,t2是c,那么用Signal表述就是 [a, b, c, …]。

在这种时空观下,原有的概念可以被很好地囊括进去,一如牛顿的经典力学是相对论力学的一个子集一样。比如,一个值为x常量,可以被视作随着时间变化的一个恒定的数据流,用Signal表述就是 [x, x, …]。

有了这样一个概念,我们可以以一个全新的角度去考虑代码。驱动程序运行的最原始的Signals成为 "single source of truth",我们需要做的就是对其map,filter,merge,groupBy,…等各种个样的composable transformation,派生出来一个个新的Signals,最终在输出的时候根据需要reduce。

Composable transformation一直是程序员苦苦追求的一个境界,而在FRP的世界里,它成为了一种随处可见的标配(哭)。我们稍后再给出一些composable transformation的例子。

由此,很多原本难以处理的事情可以被清晰地概念化,从而被很直观地处理。如果我们把鼠标单击的事件看成一个Signal,那么双击是在这个Signal上filter出来的,200ms(假设双击的阈值是200ms)内发生两次单击的Signal。

同理,kof97里面草薙的绝招大蛇稚 "下 后 下 前 拳",是keyup Signal在一定时间阈值内filter出来值依次是"下 后 下 前 拳"的Signal,这个Signal再和一组在某个时间点上草薙是否有足够的气发绝招的无限序列组[False, False, False, True, True …]组成的Signal一merge,再map一下,就是一个是否发绝招的Signal。

keyup: -r-下--上---下-下-后--下--前--拳-拳--
buffer:----r下上------------下下后下前拳拳--
气够否: -F--F--F--T--T--T--T--T--T--T--F--
大蛇稚: -F--F--F--F--F--F--F--F--F--F--T--

当然,本文的主角不是Elm,所以让我们跳过Elm,来讲讲概念上相同,实现上有些差异的Observable,它是ReactiveExtension里面最重要的一个概念。Elm和ReactiveExtensions最大的不同是,前者是一门语言,后者是与语言无关的一组概念和思想,以及这个思想在各个已知语言的实现。对Elm感兴趣的读者可以访问:elm-lang.org获取更多细节,以及看Evan Czaplicki在StrangeLoop上的精彩演讲:Taxonomy of FRP: controlling time and space(youtube,自备tizi)。

Observable从名字上看大概可以猜到是从Observer pattern演化而来的。典型的observer pattern在运行时是这样一个时序:

整个过程是同步完成的。而Observable将这个概念延伸到了异步处理当中。和Elm的Signal很像,Observable也是一个随着时间不断延展的数据流,只不过,这个数据流除了产生数据之外,还可以产生可选的错误信号和终止的信号:

任何第三方可以subscribe这个Observable,获取其数据。先不说废话,我们看一个Observable的例子(RxJs):

和上次文章里讲到的Promise类似,要创建一个Observable你需要提供一个参数为 observer 的回调函数。在这个回调函数里,你可以生成三种事件:

  • onNext:产生下一个数据
  • onError:产生错误信号。注意一旦onError发生,Observable随后会调用你提供的dispose方法,来清理回收相关的资源(如果需要的话)
  • onCompleted:产生结束信号。当这个信号发生后,Observable的生命周期结束,dispose方法会被调用进行清理回收。

在你的回调函数结束之前,你可以返回一个函数(可选),这个函数会在Observable进行 dispose 的时候被调用。

嗯,一个Obervable的定义就这么简单,和Promise相比,并没有复杂多少。

在使用方面,Observable是lazy的。cold Observable只有在 subscribe 的那一刻才被调用,hot Observable只有在 connect 发生的那一刻才开始服务。

(要访问这段代码,请移步:jsbin.com/duqaya/5/edit)

至于什么是cold Observable,什么是hot Observable,聪明如你看了代码也猜了个八九不离十:一个Observable一旦被 publish 出去,便成了hot Observable,从 connect 的时刻起,不管有没有人 subscribe,就一直在生成下一个数据,直至 onError 或者 onCompleted 为止。在不同时间节点连接上来的subscriber,会获得那个时间节点起所有的数据。嗯,典型的 Pub/Sub

在上面的例子里我们还注意到两个新的函数:intervalmap。这是Observable真正强大的地方:它不仅提供了一种思想核心(value over time),还提供了围绕着这个核心的生态圈:让人眼花缭乱的各式操作。

interval必多说,在间隔的时间(500ms)内,吐出[0, 1, …]这样一个序列;map用marble diagram表述,是这样一个概念:

(更多marble diagram,见:rxmarbles.com)

如果你翻看文档,微软为Observable精心定义了上百种chainable的操作,可以应付大部分使用的场景。参见:reactivex.io/documentation/operators.html。你当然也可以定义自己的操作,来扩展Observable的能力。我们都知道Wirth教授那著名的 "程序=算法+数据结构",如今,数据结构(Observable)和算法(operations)都给我们了,那我们能干点啥?

我们以Observable一个经典的例子来结束本文:

(访问代码请移步:jsbin.com/leroru/edit)

稍稍解释一下代码:

  • 为了便于标注Dom element,我使用了jQuery经典的$前缀;为了便于标注Observable,我使用了$后缀,你不必如此撰写代码
  • R.pipe 是ramda.js的一个函数,如果经常做函数式编程的同学应该知道,它生成一个依次执行传递进来的函数的函数。在这个例子里,生成了一个函数,创建一个li节点,然后将其append到dom里。
  • throttleInput$这个Observable是这样一个序列:
    • 首先生成一个search input下的keyup的数据流 [a, b, c, d, delete, d, e, …]
    • 然后将其pluck成输入框里的文字 [a, ab, abc, abcd, abc, abcd, abcde, …]
    • 然后filter出长度大于2的文字 [abc, abcd, abc, abcd, abcde, …]
    • 然后在一个时间间隔内仅仅emit一个数据 [abc, abc, abcde, …],这是一个backpressure的机制(见下图debounce)
    • 然后仅仅返回不同的值(删了d,又按下d)[abc, abcde, …](见下图distinct)
  • suggestion$在throttleInput$基础上做了个 flatMapLatest(searchWiki),将 [abc, abcde, …] 转换成 [abc在wiki搜索的结果,abcde在wiki搜索的结果, …]
  • searchWiki 里的 Rx.DOM.jsonpRequest(url) 也是个Observable,所以你可以用其operator: retry(见下图retry)。

几个marble diagram:

是不是很神奇?这四十多行清晰易懂,各种race condition都被消弭于无形的代码,在jQuery里,据说需要九百多行代码才能完成。你愿意写哪种代码呢?

注意,Observable是一种思想,而非一种实现,以上是RxJs的实现,我仅仅将其应用在前端而已。实际上在java/clojure/C#等代码中,都可以以相同的方式使用Observable,当然,你也可以将RxJs应用在node程序中。这是个 一次学习,到处受益 的思想。嗯,先写这么多,下次我们再讲讲如何用Observable的思想来考虑问题。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2015-09-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 程序人生 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档