谈谈FRP和Observable(二)

有些读者看了上篇文章之后第一个问题就是「这货performance如何,吃不吃内存」。仿佛他们一下子看穿了Signal/Observable的「软肋」:低效且内存占用高(潜台词是能不能跟我手写的C代码比)。对此,我得先瞎扯几句我的观点。我们看一门新技术的前景,套用当前的俗话,就是:「先问对不对,再谈好不好」。软件领域很重要的一句话是:

Simplicity matters.

从Simplicity matters这个角度看,即便用它写出的代码效率不高(我很怀疑这一论断),内存开销太大(也存疑),但四十多行的几乎无法写错的直观代码(见上一篇文章最后的typeahead的例子),无论从何种角度看都好过近千行复杂的代码。何况,在当今这样一个摩尔定律被打破,软件几乎无法坐等硬件主频升级(Intel多久没升过主频了?)而获得效率上的红利的时代,谁能在并发/异步这样的场景下表现优异,谁就坐拥了天下。对于代码而言,上层实现抽象度越大,下层并发的潜力就越大。

还有人谈到Observable看上去就是在做stream processing,而nodejs本身就自带stream,一切IO(比如network,file system,…)都是在处理stream,二者有何不同?node的stream是和unix哲学紧密契合的概念,非常好用,很简单,容易使用,这是它的优点;但它局限在IO,通用性不如Observable,而且提供的操作也仅仅限于pipe等最基础的操作,虽然有 event-stream 这样的第三方库加入了大量的实用操作(如map,join,split,merge等),但其功能丰富程度远不如Observable,为composition所做的努力也远不如Observable。

Observable是在思想上全面革新的一件利器,从上一篇文章大家应该能有所体会。这种思想还带来一个很大的好处,就是:learn once, write everywhere。我们拿Observable和设计模式来类比。设计模式的思想,你学会了以后,写java能用,写python能用,在读别人的代码时,遇到某个模式,你一下子就能大概知道作者的意图,这是设计模式作为一种思想的好处。Observable在此之上更进一步:我帮你统一思想,还帮你统一API。当你实现一个Decorator时,java的实现和C++的实现肯定因人而异,略有不同。而Observable定义了上百个API,只要相应的语言实现了这些API,那么,C#的代码和javascript的代码并没有太多语义上的区别,仅仅是语法的差别而已。你可以很容易把C#的例子转换成javascript的例子,你也可以在前端使用javascript处理Observable,在后端使用java处理Observable,这便是

它对开发者的好处不是不言而喻的。

另外一些读者的担心是Observable是不是只能应用在很小的一些场景下才能应用。今天的文章本来就计划给出更多的例子来探讨FRP和Observable的应用场景。我们先举几个实际的例子看看Observable如何去应对,然后再做个总结。

案例剖析

案例一:处理todo list

假设我们有这样一个应用,从数据库里读入之前撰写的todo items,显示给用户,并且允许用户添加新的todo。用户可以敲 enter 或者 click Add 按钮输入;如果单击任意todo item,会更改其是否完成的状态。此外,todo list不允许重复。

这虽然是个很简单的例子,相信每个人都会写(原生的不会,至少会用jquery写吧),但要写得直观,简洁,并非易事;而且,代码会东一块,西一块,并不统一,还很容易在事件监听和创建/删除节点时产生memory leak。

如果用RxJs处理,可以这么写:

让我来解释一下核心代码:

  • render在Observable addTodo$ 产生新数据的时候重绘整个list(这里如果使用virtual dom,会大大提高performance)
  • addTodo$ 是一个 Observable,我们用 Rx.Subject 生成。这是所有todo item的唯一信息源。它由几部分组成:
    • 首先是已有的数据。这里我们用 get_existing_list 这样一个函数模拟数据库读取。
    • 然后是按回车或者点 Add 按钮添加的 todo item。
    • 最后是在todo item上单击,产生的新的状态变化的todo item
  • 我们对 addTodo$scan 操作,将所有历史数据除重并聚合起来
  • 最后使用 subscribe 进行 render

http://jsbin.com/goxulu/edit

案例二:Lazy loading

这个例子处理scroll up/down 事件,然后按需加载数据,不算很难,不多说。

http://jsbin.com/noguzu/edit?html,js,output

上面两例都是UI层面的,因为我个人对animation研究不多,所以就没有献丑将animation也加入进来。Observable在前端一个很重头的使用是完美地同步 event + action + animation。当一个事件发生时,我们要产生一个异步的动作,然后再用animation提升体验。event是异步的,处理event会引入新的异步的action,之后再引入异步的animation。这几重异步如果仅仅发生一次,或者,animation结束前不允许发生新的event,还比较容易用promise处理;但event是一个永不停歇的流,很可能下次处理event的action结束后,新的animation开始时,之前的event的animation还没有结束。这样的race condition被视觉化之后,体验非常糟糕,要想圆满处理,得花好多功夫,在各种各样的state之间进行脑细胞绞杀式的同步。使用Observable,可以将这个过程大大简化,你只需要挑选合适的operator就可以了。

案例三:data collection

在服务器端,只要你勤于思考,也能发现Observable的广阔的用武之地。比如我要做一个服务,定期从若干台服务器中获取(pull)资源使用使用信息。我们希望:

  • 每个tick(100ms)请求一下服务器的资源使用情况
  • 如果上个tick的结果还未返回,而下个tick来临,则忽略下个tick,不发请求
  • 如果某个tick的结果出现异常(比如网络错误),那么直接忽略
  • 所有收集到的信息缓存5s,或者100条记录,然后再进一步处理(可能是发给下一个服务)

下面是一个模拟的例子:

代码在:http://jsbin.com/yudeqo/4/edit?html,js,console

在一个真实的环境下 getMetrics() 可能是一个async http request(或者更高效的话,tcp request,假设连接已经建立)。这里我们使用一个带有 setTimeout 的promise来模拟。真实的世界并不美好,所以我用了 boom() 来模拟潜在的失败。

getMetricObservable() 里,每个tick产生时,我使用了一个 inFlight 变量来控制一个tick是否要产生一个 getMetrics() 请求。inFlight 在调用 getMetrics() 前后被设置。

tick       ---t---t---t--------
filter     ---t---t------------
do         ---t---t------------
map        -----g-------g------
do         -----g-------g------

# sideEffect
inFlight   FFFTTFFTTTTTTFFFFFFF

在Observable里 do 可以用来处理side effect(副作用)。这是AOP(Aspect Oriented Programming)思想的一种体现。我们当然可以在map里面处理 inFlight的改变,然而这样会让整个代码变得很丑陋,而且失去了很多优势(比如并发处理)。函数式编程很重要的一个思想是

把 side effect 关在笼子里

如果side effect不可避免,那么,把它们放在集中的地方,显式地告诉编译器(或者库)这段代码有副作用,是最好的方式。这样,那些没有副作用的代码,编译器依旧可以尽量优化。

do 在Observable里,遇到上游的Observable传过来的内容,不做任何处理,向下游传递,同时,在函数体内做相应的副作用的处理。比如你要 console.log 一些中间状态,do是最好的选择。在这段代码里,每个 tick 或者每次 map 返回一个值,do 都相应改变一下 inFlight 的状态。

正常情况下,当发生错误,错误会一路bubble到 subscribe Observable的地方。在这里,我们不希望错误被bubble up,所以用 Rx.Observable.onErrorResumeNext(onErrorResumeNext有没有VB的赶脚?~)来忽略错误。当然,你也可以 retry(),但这里没有必要。

这就有了一台服务器的Observable。多台服务器我们只需要把他们的Observable merge() 一下,然后 bufferWithTimeOrCount,就实现了我们的需求。下面是 onErrorResumeNextbufferWithTimeOrCount 的 marble图:

在这里:

我们顺带对原始数据做了个处理,把一个带着 {dt:…, metrics: […]} 的 object,转化成了一个形如 [{}, …] 的数组。这样,当你后续的处理需要单独处理某种metric,如CPU,可以很方便处理。这里只是展示一下,如果在Observable里要对数据做transformation,也是非常简单的。注意,这里我们没有修改 data.metrics.map 里每个数据(可以这么做但绝对不推荐!),而是使用prototype inheritance创建了新的数据(Object.create),prototype inheritance是copy-on-write的,我们这里没有动 metric 原有的数据,只是添加了新的数据 dt,所以实际上没有拷贝原有数据,效率很高。

这个例子是纯 Nodejs 的例子,放在 jsbin 里,只是为了大家能很直观地运行和观察结果。Observable在服务器端有很多适用的场景,任何和event流相关的事情都可以考虑用其实现。

总结

处理Async并非易事。你要很小心地设计你的代码,考虑这些情况:

  • race conditions
  • 内存泄漏(比如一个event handler,bind后忘记unbind)
  • 管理复杂的状态机
  • 错误处理

而Observable能帮你减轻这些负担,把你的精力集中在如何描述问题的解决方案上,而非如何去管理复杂的状态,处理要命的race condition。

在处理Observable时,我们经常遇到一个数据流分解成多个数据流,或者多个数据流合并成一个数据流,而后者往往是异步处理让人头疼的事情。Observable提供了一些手段,可以参考:

  • 你可以concatAll,如果多个Observable的数据是要保留先后顺序的(类比priority queue)
  • 也可以mergeAll,如果多个Observable的数据不需要保留顺序,先进先出(类比traffic merge)
  • 还可以switch,你想在多个事件中仅仅处理最后发生的那个,忽略其他

原文发布于微信公众号 - 程序人生(programmer_life)

原文发表时间:2015-09-28

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏杨建荣的学习笔记

excel查询数据的技术实现选择 (r7笔记第73天)

最近有一个高中同学问我一件事情,就是他们目前使用excel需要经常查很多数据,但现在使用excel的筛选功能感觉比较费劲。想问问我有没有什么改进 的方法,对于我...

3315
来自专栏大宽宽的碎碎念

如何深入理解开源项目从小代码集看起聚焦请先看文档关注资源的生命周期找一个好工具建立调试环境看代码很累,要坚持

3116
来自专栏web前端教室

javascript中那些可以连成片的点

JavaScript的提高,是一点一滴的提高,这些点滴连接成线,进而连接成为一个面。 这个“面”的知识你都会了之后,会首先从某个点上形成突破再提高,然后这些再提...

1786
来自专栏CSDN技术头条

服务端接口自动化测试工具大全及适配环境解析

在互联网时代,服务端测试已经成为一个重要的产品保障手段,各对此公司实施的方法和技术也不同,本文我们就来讨论一下。 互联网服务端接口自动化是各个公司都需要一部分业...

3606
来自专栏芋道源码1024

Dubbo源码解析 —— Zookeeper 订阅

前言 上周写完了服务暴露总结之后发现遗漏了一个很重要的点,在dubbo源码解析-zookeeper连接中我们对面试高频题 dubbo中zookeeper做注册...

2647
来自专栏进击的程序猿

The Clean Architecture in PHP 读书笔记(一)

框架是非常好的,可以帮助我们快速的开发,但是前期的学习成本往往很高,特别是如果想要深入理解框架,需要花费大量的经历。

643
来自专栏逍遥剑客的游戏开发

Nebula3中的Message

1695
来自专栏xingoo, 一个梦想做发明家的程序员

C++库大全

基础类 1、 Dinkumware C++ Library 参考站点:http://www.dinkumware.com P.J. Plauger编写的高品质...

1906
来自专栏小鄧子的技术博客专栏

Architecting Android with RxJava

三个月以来,我翻译了一些关于RxJava的文章,说实话这些翻译,真的搞得我很头疼,那么现在是时候回来写点什么了。

651
来自专栏技术点滴

Linux内核源码分析方法

Linux内核源码分析方法 一、内核源码之我见 Linux内核代码的庞大令不少人“望而生畏”,也正因为如此,使得人们对Linux的了解仅处于泛泛的层次。如果想透...

2557

扫描关注云+社区