探秘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 相关文章推荐
Prototype使用指南之dom.js
Jan 10 Javascript
Js 冒泡事件阻止实现代码
Jan 27 Javascript
Js操作Select大全(取值、设置选中等等)
Oct 29 Javascript
js调试工具console.log()方法查看js代码的执行情况
Aug 08 Javascript
JS点击链接后慢慢展开隐藏着图片的方法
Feb 17 Javascript
jquery实现两边飘浮可关闭的对联广告
Nov 27 Javascript
javascript和jQuery实现网页实时聊天的ajax长轮询
Jul 20 Javascript
Javascript发送AJAX请求实例代码
Aug 21 Javascript
angular实现IM聊天图片发送实例
May 08 Javascript
JS按钮闪烁功能的实现代码
Jul 21 Javascript
基于Vue开发数字输入框组件
Dec 19 Javascript
详解如何在Javascript中使用Object.freeze()
Oct 18 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连接Oracle数据库
2006/10/09 PHP
PHP foreach循环使用详解与实例代码
2010/05/08 PHP
功能强大的PHP POST提交数据类
2016/07/15 PHP
PHP基于phpqrcode类生成二维码的方法示例详解
2020/08/07 PHP
JavaScript基本对象
2007/01/11 Javascript
js 纯数字不重复排列的另类方法
2010/07/17 Javascript
js关闭浏览器窗口及检查浏览器关闭事件
2013/09/03 Javascript
javascript中动态加载js文件多种解决办法总结
2013/11/15 Javascript
jquery解析json格式数据的方法(对象、字符串)
2015/11/24 Javascript
喜大普奔!jQuery发布 3.0 最终版
2016/06/12 Javascript
js判断请求的url是否可访问,支持跨域判断的实现方法
2016/09/17 Javascript
Bootstrap Modal遮罩弹出层(完整版)
2016/11/21 Javascript
使用Ajax生成的Excel文件并下载的实例
2016/11/21 Javascript
学习使用bootstrap的modal和carousel
2016/12/09 Javascript
bootstrap下拉菜单使用方法解析
2017/01/13 Javascript
详解vue 模版组件的三种用法
2017/07/21 Javascript
探究react-native 源码的图片缓存问题
2017/08/24 Javascript
React-Native中禁用Navigator手势返回的示例代码
2017/09/09 Javascript
web前端开发中常见的多列布局解决方案整理(一定要看)
2017/10/15 Javascript
VUE重点问题总结
2018/03/19 Javascript
7个好用的JavaScript技巧分享(译)
2019/05/07 Javascript
微信小程序 WXML节点信息查询详解
2019/07/29 Javascript
Vue如何提升首屏加载速度实例解析
2020/06/25 Javascript
addEventListener()和removeEventListener()追加事件和删除追加事件
2020/12/04 Javascript
python实现微信发送邮件关闭电脑功能
2018/02/22 Python
多个应用共存的Django配置方法
2018/05/30 Python
Python实现繁?转为简体的方法示例
2018/12/18 Python
PyQt+socket实现远程操作服务器的方法示例
2019/08/22 Python
Python编程快速上手——strip()函数的正则表达式实现方法分析
2020/02/29 Python
Django模板获取field的verbose_name实例
2020/05/19 Python
python报错TypeError: ‘NoneType‘ object is not subscriptable的解决方法
2020/11/05 Python
英国折扣高尔夫商店:Discount Golf Store
2019/11/19 全球购物
药学专业大学生自荐信
2013/09/28 职场文书
班主任经验交流会主持词
2014/04/01 职场文书
2014年高校辅导员工作总结
2014/12/09 职场文书
幼儿园教师培训心得体会
2016/01/21 职场文书