谈谈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 条评论
登录 后参与评论

相关文章

来自专栏web前端教室

如何学习才能见到效果

几乎每个新入行的人都会问,有没有什么好的或正确的学习方法? 有的。 但误区在于,他们总是把好的或正确的学习方法,当成了“快”的学习方法。好或正确,并不意味着快;...

1898
来自专栏牛客网

百度前端实习岗

1000
来自专栏蓝天

高质量C++编程补充条款

介绍高质量C++编程的书籍很多,而且都非常好,这里主要针对已有书籍较少涉及到的代码格式条款进行补充。代码是程序员脸面,清清爽爽和干干净净的代码是程序员高职业素质...

922
来自专栏SAP最佳业务实践

SAP最佳业务实践:FI–总账(156)-5显示、对账

4.6 S_ALR_87012289显示简要凭证日记帐 简要凭证日记帐以表的形式为所选凭证显示凭证抬头和项目中最重要的数据。该清单可用作简要日记帐,并且与科目...

3617
来自专栏UML

数据流程图 (DFD) 示例:食品订购系统

数据流图也称为气泡图。它通常用作创建系统概述的初步步骤,而不需要详细介绍,以后可以将其作为自上而下的分解方式进行详细说明。DFD显示将从系统输入和输出的信息类型...

1546
来自专栏带你撸出一手好代码

使用测试用例来约束自己的代码

写测试代码这种事情 ,以前只在网上和书上看到过, 自己从来没有写过。 每当看到那些世界顶级程序员编写的技术书籍中出现“测试用例”“测试代码”的字样或者一些行业的...

3256
来自专栏牛客网

老虎证券 iOS一面

2@property (weak, nonatomic) id delegate;

481
来自专栏喔家ArchiSelf

IOT语义互操作性之API接口

这个系列文章描述了一个单一的语义数据模型来支持物联网和建筑、企业和消费者的数据转换。 这种模型必须简单可扩展, 以便能够在各行业领域之间实现插件化和互操作性。 ...

723
来自专栏互联网杂技

以后有机会写框架用得着的

在这个js框架随处乱跑的时代,你是否考虑过写一个自己的框架?下面的内容也许会有点帮助。 一个框架应该包含哪些内容? 1. 语言扩展 大部分现有的框架都提供了这部...

2595
来自专栏闻道于事

C语言课程设计(成绩管理系统)

C语言课程设计(成绩管理系统) 翻到了大学写的C语言课程设计,缅怀一下 内容: 增加学生成绩 查询学生成绩 删除 按照学生成绩进行排序 等 1 #inclu...

3715

扫码关注云+社区