有些读者看了上篇文章之后第一个问题就是「这货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处理,可以这么写:
让我来解释一下核心代码:
addTodo$
产生新数据的时候重绘整个list(这里如果使用virtual dom,会大大提高performance)addTodo$
是一个 Observable,我们用 Rx.Subject
生成。这是所有todo item的唯一信息源。它由几部分组成:get_existing_list
这样一个函数模拟数据库读取。Add
按钮添加的 todo item。addTodo$
做 scan
操作,将所有历史数据除重并聚合起来subscribe
进行 render
。案例二: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)资源使用使用信息。我们希望:
下面是一个模拟的例子:
代码在: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
,就实现了我们的需求。下面是 onErrorResumeNext
和 bufferWithTimeOrCount
的 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并非易事。你要很小心地设计你的代码,考虑这些情况:
而Observable能帮你减轻这些负担,把你的精力集中在如何描述问题的解决方案上,而非如何去管理复杂的状态,处理要命的race condition。
在处理Observable时,我们经常遇到一个数据流分解成多个数据流,或者多个数据流合并成一个数据流,而后者往往是异步处理让人头疼的事情。Observable提供了一些手段,可以参考: