探秘vue-rx 2.0(推荐)


Posted in Javascript onSeptember 21, 2018

前一段时间,我写了两篇文章,一篇是对目前前端主流视图框架的思考,一篇是深入使用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这次的升级很好地满足了我对复杂应用开发的需求了。

本文示例代码参见:这里

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
Ext 今日学习总结
Sep 19 Javascript
这些年、我收集的JQuery代码小结
Aug 01 Javascript
js函数返回多个返回值的示例代码
Nov 05 Javascript
可恶的ie8提示缺少id未定义
Mar 20 Javascript
Javascript数组操作函数总结
Feb 05 Javascript
js中this用法实例详解
May 05 Javascript
jquery.map()方法的使用详解
Jul 09 Javascript
jquery.validate使用详解
Jun 02 Javascript
jQuery实现公告新闻自动滚屏效果实例代码
Jul 14 Javascript
微信小程序实现顶部普通选项卡效果(非swiper)
Jun 19 Javascript
vue项目中使用Hbuilder打包app 设置沉浸式状态栏的方法
Oct 22 Javascript
vue通过v-html指令渲染的富文本无法修改样式的解决方案
May 20 Javascript
Vue-Quill-Editor富文本编辑器的使用教程
Sep 21 #Javascript
vue+axios实现文件下载及vue中使用axios的实例
Sep 21 #Javascript
vue-rx的初步使用教程
Sep 21 #Javascript
基于Vue 服务端Cookies删除的问题
Sep 21 #Javascript
Vue中插入HTML代码的方法
Sep 21 #Javascript
node.js调用C++函数的方法示例
Sep 21 #Javascript
Vue中Quill富文本编辑器的使用教程
Sep 21 #Javascript
You might like
PHP读取目录下所有文件的代码
2008/01/07 PHP
PHP Zip压缩 在线对文件进行压缩的函数
2010/05/26 PHP
PHP 年龄计算函数(精确到天)
2012/06/07 PHP
php初始化对象和析构函数的简单实例
2014/03/11 PHP
理解PHP中的stdClass类
2014/04/18 PHP
高性能PHP框架Symfony2经典入门教程
2014/07/08 PHP
PHP采用超长(超大)数字运算防止数字以科学计数法显示的方法
2016/04/01 PHP
自定义min版smarty模板引擎MinSmarty.class.php文件及用法
2016/05/20 PHP
php封装单文件上传到数据库(路径)
2017/10/15 PHP
php校验公钥是否可用的实例方法
2019/09/17 PHP
thinkphp5实现微信扫码支付
2019/12/23 PHP
关于Anemometer图形化显示MySQL慢日志的工具搭建及使用的详细介绍
2020/07/13 PHP
Jquery写一个鼠标拖动效果实现原理与代码
2012/12/24 Javascript
jquery操作下拉列表、文本框、复选框、单选框集合(收藏)
2014/01/08 Javascript
Javascript动态创建div的方法
2015/02/09 Javascript
Web Uploader文件上传插件使用详解
2016/05/10 Javascript
谈谈target=_new和_blank的不同之处
2016/10/25 Javascript
js 将canvas生成图片保存,或直接保存一张图片的实现方法
2018/01/02 Javascript
JavaScript中将值转换为字符串的五种方法总结
2019/06/06 Javascript
JS实现商城秒杀倒计时功能(动态设置秒杀时间)
2019/12/12 Javascript
微信小程序自定义底部弹出框功能
2020/11/18 Javascript
Python中每次处理一个字符的5种方法
2015/05/21 Python
基于python yield机制的异步操作同步化编程模型
2016/03/18 Python
python3.4控制用户输入与输出的方法
2018/10/17 Python
pycharm导入源码的具体步骤
2020/08/04 Python
Python+Selenium随机生成手机验证码并检查页面上是否弹出重复手机号码提示框
2020/09/21 Python
分享unittest单元测试框架中几种常用的用例加载方法
2020/12/02 Python
美国汽车轮胎和轮毂销售网站:Tire Rack
2018/01/11 全球购物
Expedia韩国官网:亚洲发展最快的在线旅游门户网站
2018/02/26 全球购物
团队口号大全
2014/06/06 职场文书
作风年建设汇报材料
2014/08/14 职场文书
民族学专业大学生职业规划范文:清晰未来的构想
2014/09/20 职场文书
晋江市人民政府党组群众路线教育实践活动整改方案
2014/10/25 职场文书
幼儿园庆元旦主持词
2015/07/06 职场文书
公司会议开幕词
2016/03/03 职场文书
Python基本的内置数据类型及使用方法
2022/04/13 Python