Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

探秘 vue-rx 2.0 #39

Open
xufei opened this issue Nov 10, 2016 · 4 comments
Open

探秘 vue-rx 2.0 #39

xufei opened this issue Nov 10, 2016 · 4 comments

Comments

@xufei
Copy link
Owner

xufei commented Nov 10, 2016

前一段时间,我写了两篇文章,一篇是对目前前端主流视图框架的思考:#37,一篇是深入使用RxJS控制复杂业务逻辑的:#38,在这两篇中,我分别提到:

  • 期望在复杂业务逻辑方面使用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这次的升级很好地满足了我对复杂应用开发的需求了。

本文示例代码参见:这里

@limichange
Copy link

能否将Rxjs理解为是比Vuex更好的一种数据管理的选择?

@xufei
Copy link
Owner Author

xufei commented Dec 14, 2016 via email

@gengjiawen
Copy link

请问使用vue-rx兄弟间组件数据共享有没有好的实践?

@mlyknown
Copy link

mlyknown commented Sep 7, 2017

@gengjiawen 兄弟间组件数据共享和 vue-rx并没有直接关系, 可以把Observable提到比组件更高的层级,组件去subscribe 就好了

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants