探秘vue-rx 2.0(推荐)
前一段时间,我写了两篇文章,一篇是对目前前端主流视图框架的思考,一篇是深入使用rxjs控制复杂业务逻辑的,在这两篇中,我分别提到:
- 期望在复杂业务逻辑方面使用rxjs,更好地进行抽象,但是视图上使用轻量mvvm以达到快速开发的目的。
- 目前vuejs中,如果要结合rxjs,可能需要手动订阅和取消订阅,写起来还是没有cyclejs方便。
最近,vuejs社区升级了vue-rx这个库,实现了比较方便地把vuejs和rxjs结合的能力。
我们来详细了解一下。
在视图上绑定observable
vuejs本身不是基于rxjs这一套理念构建的,如果不借助任何辅助的东西,可能我们会需要干这么一些事情:
- 手动订阅某些observable,在observer里面,把数据设置到vue的data上
- 在视图销毁的时候,手动取消订阅
在业务开发中,我们最常用的是绑定简单的observable,在vue-rx中,这个需求被很轻松地满足了。
与早期版本不同,vue-rx 2.0在vue实例上添加了一个subscriptions属性,里面放置各种待绑定的observable,用的时候类似data。
比如,我们可以这么用它:
rx-simple.vue
<template> <div> <h4>single value</h4> <div>{{single$}}</div> <h4>array</h4> <ul> <li v-for="item of arr0$">{{item}}</li> </ul> <ul> <li v-for="item of arr1$">{{item}}</li> </ul> <h4>interval</h4> <div>{{interval$}}</div> <h4>high-order</h4> <div>{{high$}}</div> </div> </template> <script> import { observable } from 'rxjs/observable' import 'rxjs/add/observable/of' import 'rxjs/add/observable/from' import 'rxjs/add/operator/toarray' import 'rxjs/add/observable/interval' import 'rxjs/add/observable/range' import 'rxjs/add/operator/map' import 'rxjs/add/operator/mergeall' const single$ = observable.of(math.pi) const arr0$ = observable.of([1, 1, 2, 3, 5, 8, 13]) const arr1$ = observable.from([1, 1, 2, 3, 5, 8, 13]).toarray() const interval$ = observable.interval(1000) const high$ = observable.range(1, 5) .map(item => observable.interval(item * 1000)) .mergeall() export default { name: 'rx-simple', subscriptions: { single$, arr0$, arr1$, interval$, high$ } } </script>
这个demo里面,演示了四种不同的rx数据形态。其中,single$和interval$虽然创建方式不同,但实际上用的时候是一样的,因为,对它们的订阅,都是取其最后一个值,这两者的区别只是,一个不变了,一个持续变,但界面展示的始终是最后那个值。
关于数组,初学者需要稍微注意一下,从同样的数组,分别通过observable.of和observable.from出来的形态是大为不同的:
- of创建的这个,里面只有一个值,这个值是个数组,所以,订阅它,会得到一个数组
- from创建的这个,里面有若干个值,每个值是由数组中的元素创建的,订阅它,会一次性得到多个值,但展示的时候只会有最后一个,因为前面的都被覆盖掉了
那么,这个high$代表什么呢?
- range操作,创建了一个流,里面有多个简单数字
- map操作,把这个流升级为二阶,流里面每个元素又是一个流
- mergeall操作,把其中的每个流合并,降阶为一阶流,流里面每个元素是个简单数字
如果说不mergeall,直接订阅map出来的那个二阶流,结果是不对的,vue-rx只支持一阶订阅绑定,不支持把高阶流直接绑定,如果有业务需要,应当自行降阶,通过各种flat、concat、merge操作,变成一阶流再进行绑定。
将vue $watcher转换为observable
上面我们述及的,都是从observable的数据到vue的reactivesetter和getter中,这条路径的操作已经很简便了,我们只需把observable放在vue实例的subscriptions里面,就能直接绑定到视图。
但是,反过来还有一条线,我们可能会需要根据某个数据的变化,让这个数据进入一个数据流,然后进行后续运算。
例如:有一个num属性,挂在data上,还有一个数据num1,表达:始终比num大1这么一件事。
当然,我们是可以直接利用computed property去做这件事的,为了使得我们这个例子更有说服力,给它这个加一计算添加一个延时3秒,强行变成异步:始终在num属性确定之后,等3秒,把自己变成比num大1的数字。
这样,computed property就写不出来了,我们可能就要手动去$watch这个num,然后在回调方法中,去延时加一,然后回来赋值给num1。
在vur-rx中,提供了一个从$watch创建observable的方法,叫做$watchasobservable,我们来看看怎么用:
rx-watcher.vue
<template> <div> <h4>watch</h4> <div> <button v-on:click="num++">add</button> source: {{num}} -> result: {{num$}} </div> </div> </template> <script> import 'rxjs/add/operator/pluck' import 'rxjs/add/operator/startwith' import 'rxjs/add/operator/delay' export default { name: 'rx-watch', data() { return { num: 1 } }, subscriptions() { return { num$: this.$watchasobservable('num') .pluck('newvalue') .startwith(this.num) .map(a => a + 1) .delay(3000) } } } </script>
这个例子里面的num$经过这么几步:
- this.$watchasobservable('num'),把num属性的变动,映射到一个数据流上
- 这个数据流的结果是一个对象,里面有newvalue和oldvalue属性,我们通常情况下,要的都是newvalue,所以用pluck把它挑出来
- 注意,这个检测的只是后续变动,对于已经存在的值,是$watch不到的,所以,用startwith,把当前值放进去
- 然后是常规的rx运算了
那么,这件事的原理是什么呢?
我们知道,vue实例中,data上的属性都会存在reactivesetter,所以它被赋值的时候,就会触发这个setter,所以,$watchasobservable的内部只需根据数据变动,生成一个observable就可以了。
$watchasobservable的方法签名如下:
$watchasobservable(exporfn, [options])
这个options,跟vue的$watch方法的options一样。
有时候,我们会有这样的情况:在组件实例化的时候,数据流由于缺少某些条件,可能还没法创建。
比如说,某个组件,依赖于路由上面的某个参数,这时候,可能你不知道怎么去初始化绑定。
其实,产生这样的想法,本身就错了,因为没有用rx的理念去思考问题。想一下下面这句话:
数据流的定义,与初始条件是否具备无关。
初始条件其实也只是整个数据流管道中的一节,如果初始不确定的话,我们只要给它留一个数据入口就好了,后续的流转定义可以全部写得出来。
const taskid$ = new subject() const task$ = taskid$ .distinctuntilchanged() .switchmap(id => this.getinitialdata(id))
然后,在路由变更等事件里,往这个taskid$里面next当前的id就可以了。通过这种方式,我们就可以把task$直接绑定到界面上。
或者,taskid$也可以通过在路由上面的watch转化而成,只是不能直接用$watchasobservable,可以考虑改进一下这种情况。
这样可以实现组件canreuse的情况下,改动路由参数,触发当前页面的数据刷新,实现视图的更轻量级的刷新。
将dom事件转化为observable
使用rxjs可以直接把dom事件转化为observable,vue-rx也提供了一个类似的方法来做这个事,不过我没理解这两个东西有什么差异?具体参见官方示例吧。
构建优化
关注vue-rx的readme,可以发现,目前推荐使用绑定的方式是这样:
import vue from 'vue' import rx from 'rxjs/rx' import vuerx from 'vue-rx' // tada! vue.use(vuerx, rx)
但这样会有一个问题,import的是rxjs/rx,我们看到,这个文件里把所有可以被挂接到rx对象上的东西都import进来了,这会导致构建的时候没法tree-shaking,用不到的那些操作符也被构建进来了,一个简单的demo,可能构建结果也有200多k,这还是太大了。
我们查看一下vue-rx的源码,发现传入的这个rx是怎么使用的呢?
var obs$ = rx.observable.create(function (observer) { ... // returns function which disconnects the $watch expression var disposable if (rx.subscription) { // rx5 disposable = new rx.subscription(unwatch) } else { // rx4 disposable = rx.disposable.create(unwatch) }
这里,其实只是要使用observable和subscription这两个东西,所以我们可以改成这样:
import vue from 'vue' import { observable } from 'rxjs/observable' import { subscription } from 'rxjs/subscription' import vuerx from 'vue-rx' // tada! vue.use(vuerx, { observable, subscription })
再试试,构建大小只有不到100k了,而且是可以正常运行的。如果用的是rx 4,需要传入的就是disposable而不是subscription。
另外,如果我们使用了$watchasobservable,还会需要引入另外一个东西:
import 'rxjs/add/operator/publish'
这是因为在$watchasobservable里面,为了共享observable,把它pubish之后refcount了,所以要引入,用不到这个方法的话,可以不引。
如果使用了$fromdomevent,还需要引入这个:
import 'rxjs/add/observable/empty'
因为$fromdomevent里面的这段:
if (typeof window === 'undefined') { return rx.observable.empty() }
小结
有了这个库之后,我们就可以比较优雅地结合vuejs和rxjs了。之前,两者之间结合的麻烦点主要在于:
在rxjs体系中,数据的进、出这两头是有些繁琐的。
所以,cyclejs采用了比较极端的做法,把dom体系也包括进去了,这样,编写代码的时候,数据就没有进出的成本,但这么做,其实是牺牲了一些视图层的编写效率。
而angular2中,用的是async这个pipe来解决这问题,这也是一种比较方便的办法,在绑定observable这一点上,跟有了vue-rx之后的vue是差不多简便的。
react体系里面也有对rxjs的适配,而且还有跟redux,mobx对接的适配,感兴趣的可以自行关注。
从个人角度出发,vue-rx这次的升级很好地满足了我对复杂应用开发的需求了。
本文示例代码参见:
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。