探秘vue-rx 2.0(推薦)
前一段時(shí)間,我寫了兩篇文章,一篇是對(duì)目前前端主流視圖框架的思考,一篇是深入使用RxJS控制復(fù)雜業(yè)務(wù)邏輯的,在這兩篇中,我分別提到:
- 期望在復(fù)雜業(yè)務(wù)邏輯方面使用RxJS,更好地進(jìn)行抽象,但是視圖上使用輕量MVVM以達(dá)到快速開發(fā)的目的。
- 目前VueJS中,如果要結(jié)合RxJS,可能需要手動(dòng)訂閱和取消訂閱,寫起來還是沒有CycleJS方便。
最近,VueJS社區(qū)升級(jí)了vue-rx這個(gè)庫,實(shí)現(xiàn)了比較方便地把VueJS和RxJS結(jié)合的能力。
我們來詳細(xì)了解一下。
在視圖上綁定Observable
VueJS本身不是基于RxJS這一套理念構(gòu)建的,如果不借助任何輔助的東西,可能我們會(huì)需要干這么一些事情:
- 手動(dòng)訂閱某些Observable,在observer里面,把數(shù)據(jù)設(shè)置到Vue的data上
- 在視圖銷毀的時(shí)候,手動(dòng)取消訂閱
在業(yè)務(wù)開發(fā)中,我們最常用的是綁定簡(jiǎn)單的Observable,在vue-rx中,這個(gè)需求被很輕松地滿足了。
與早期版本不同,vue-rx 2.0在Vue實(shí)例上添加了一個(gè)subscriptions屬性,里面放置各種待綁定的Observable,用的時(shí)候類似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>
這個(gè)demo里面,演示了四種不同的Rx數(shù)據(jù)形態(tài)。其中,single$和interval$雖然創(chuàng)建方式不同,但實(shí)際上用的時(shí)候是一樣的,因?yàn)?,?duì)它們的訂閱,都是取其最后一個(gè)值,這兩者的區(qū)別只是,一個(gè)不變了,一個(gè)持續(xù)變,但界面展示的始終是最后那個(gè)值。
關(guān)于數(shù)組,初學(xué)者需要稍微注意一下,從同樣的數(shù)組,分別通過Observable.of和Observable.from出來的形態(tài)是大為不同的:
- of創(chuàng)建的這個(gè),里面只有一個(gè)值,這個(gè)值是個(gè)數(shù)組,所以,訂閱它,會(huì)得到一個(gè)數(shù)組
- from創(chuàng)建的這個(gè),里面有若干個(gè)值,每個(gè)值是由數(shù)組中的元素創(chuàng)建的,訂閱它,會(huì)一次性得到多個(gè)值,但展示的時(shí)候只會(huì)有最后一個(gè),因?yàn)榍懊娴亩急桓采w掉了
那么,這個(gè)high$代表什么呢?
- range操作,創(chuàng)建了一個(gè)流,里面有多個(gè)簡(jiǎn)單數(shù)字
- map操作,把這個(gè)流升級(jí)為二階,流里面每個(gè)元素又是一個(gè)流
- mergeAll操作,把其中的每個(gè)流合并,降階為一階流,流里面每個(gè)元素是個(gè)簡(jiǎn)單數(shù)字
如果說不mergeAll,直接訂閱map出來的那個(gè)二階流,結(jié)果是不對(duì)的,vue-rx只支持一階訂閱綁定,不支持把高階流直接綁定,如果有業(yè)務(wù)需要,應(yīng)當(dāng)自行降階,通過各種flat、concat、merge操作,變成一階流再進(jìn)行綁定。
將Vue $watcher轉(zhuǎn)換為Observable
上面我們述及的,都是從Observable的數(shù)據(jù)到Vue的ReactiveSetter和Getter中,這條路徑的操作已經(jīng)很簡(jiǎn)便了,我們只需把Observable放在vue實(shí)例的subscriptions里面,就能直接綁定到視圖。
但是,反過來還有一條線,我們可能會(huì)需要根據(jù)某個(gè)數(shù)據(jù)的變化,讓這個(gè)數(shù)據(jù)進(jìn)入一個(gè)數(shù)據(jù)流,然后進(jìn)行后續(xù)運(yùn)算。
例如:有一個(gè)num屬性,掛在data上,還有一個(gè)數(shù)據(jù)num1,表達(dá):始終比num大1這么一件事。
當(dāng)然,我們是可以直接利用computed property去做這件事的,為了使得我們這個(gè)例子更有說服力,給它這個(gè)加一計(jì)算添加一個(gè)延時(shí)3秒,強(qiáng)行變成異步:始終在num屬性確定之后,等3秒,把自己變成比num大1的數(shù)字。
這樣,computed property就寫不出來了,我們可能就要手動(dòng)去$watch這個(gè)num,然后在回調(diào)方法中,去延時(shí)加一,然后回來賦值給num1。
在vur-rx中,提供了一個(gè)從$watch創(chuàng)建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>
這個(gè)例子里面的num$經(jīng)過這么幾步:
- this.$watchAsObservable('num'),把num屬性的變動(dòng),映射到一個(gè)數(shù)據(jù)流上
- 這個(gè)數(shù)據(jù)流的結(jié)果是一個(gè)對(duì)象,里面有newValue和oldValue屬性,我們通常情況下,要的都是newValue,所以用pluck把它挑出來
- 注意,這個(gè)檢測(cè)的只是后續(xù)變動(dòng),對(duì)于已經(jīng)存在的值,是$watch不到的,所以,用startWith,把當(dāng)前值放進(jìn)去
- 然后是常規(guī)的rx運(yùn)算了
那么,這件事的原理是什么呢?
我們知道,Vue實(shí)例中,data上的屬性都會(huì)存在ReactiveSetter,所以它被賦值的時(shí)候,就會(huì)觸發(fā)這個(gè)setter,所以,$watchAsObservable的內(nèi)部只需根據(jù)數(shù)據(jù)變動(dòng),生成一個(gè)Observable就可以了。
$watchAsObservable的方法簽名如下:
$watchAsObservable(expOrFn, [options])
這個(gè)options,跟vue的$watch方法的options一樣。
有時(shí)候,我們會(huì)有這樣的情況:在組件實(shí)例化的時(shí)候,數(shù)據(jù)流由于缺少某些條件,可能還沒法創(chuàng)建。
比如說,某個(gè)組件,依賴于路由上面的某個(gè)參數(shù),這時(shí)候,可能你不知道怎么去初始化綁定。
其實(shí),產(chǎn)生這樣的想法,本身就錯(cuò)了,因?yàn)闆]有用Rx的理念去思考問題。想一下下面這句話:
數(shù)據(jù)流的定義,與初始條件是否具備無關(guān)。
初始條件其實(shí)也只是整個(gè)數(shù)據(jù)流管道中的一節(jié),如果初始不確定的話,我們只要給它留一個(gè)數(shù)據(jù)入口就好了,后續(xù)的流轉(zhuǎn)定義可以全部寫得出來。
const taskId$ = new Subject() const task$ = taskId$ .distinctUntilChanged() .switchMap(id => this.getInitialData(id))
然后,在路由變更等事件里,往這個(gè)taskId$里面next當(dāng)前的id就可以了。通過這種方式,我們就可以把task$直接綁定到界面上。
或者,taskId$也可以通過在路由上面的watch轉(zhuǎn)化而成,只是不能直接用$watchAsObservable,可以考慮改進(jìn)一下這種情況。
這樣可以實(shí)現(xiàn)組件canReuse的情況下,改動(dòng)路由參數(shù),觸發(fā)當(dāng)前頁面的數(shù)據(jù)刷新,實(shí)現(xiàn)視圖的更輕量級(jí)的刷新。
將DOM事件轉(zhuǎn)化為Observable
使用RxJS可以直接把DOM事件轉(zhuǎn)化為Observable,vue-rx也提供了一個(gè)類似的方法來做這個(gè)事,不過我沒理解這兩個(gè)東西有什么差異?具體參見官方示例吧。
構(gòu)建優(yōu)化
關(guān)注vue-rx的readme,可以發(fā)現(xiàn),目前推薦使用綁定的方式是這樣:
import Vue from 'vue' import Rx from 'rxjs/Rx' import VueRx from 'vue-rx' // tada! Vue.use(VueRx, Rx)
但這樣會(huì)有一個(gè)問題,import的是rxjs/Rx,我們看到,這個(gè)文件里把所有可以被掛接到Rx對(duì)象上的東西都import進(jìn)來了,這會(huì)導(dǎo)致構(gòu)建的時(shí)候沒法tree-shaking,用不到的那些操作符也被構(gòu)建進(jìn)來了,一個(gè)簡(jiǎn)單的demo,可能構(gòu)建結(jié)果也有200多k,這還是太大了。
我們查看一下vue-rx的源碼,發(fā)現(xiàn)傳入的這個(gè)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)
}
這里,其實(shí)只是要使用Observable和Subscription這兩個(gè)東西,所以我們可以改成這樣:
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 })
再試試,構(gòu)建大小只有不到100k了,而且是可以正常運(yùn)行的。如果用的是Rx 4,需要傳入的就是Disposable而不是Subscription。
另外,如果我們使用了$watchAsObservable,還會(huì)需要引入另外一個(gè)東西:
import 'rxjs/add/operator/publish'
這是因?yàn)樵?watchAsObservable里面,為了共享Observable,把它pubish之后refCount了,所以要引入,用不到這個(gè)方法的話,可以不引。
如果使用了$fromDOMEvent,還需要引入這個(gè):
import 'rxjs/add/observable/empty'
因?yàn)?fromDOMEvent里面的這段:
if (typeof window === 'undefined') {
return Rx.Observable.empty()
}
小結(jié)
有了這個(gè)庫之后,我們就可以比較優(yōu)雅地結(jié)合VueJS和RxJS了。之前,兩者之間結(jié)合的麻煩點(diǎn)主要在于:
在RxJS體系中,數(shù)據(jù)的進(jìn)、出這兩頭是有些繁瑣的。
所以,CycleJS采用了比較極端的做法,把DOM體系也包括進(jìn)去了,這樣,編寫代碼的時(shí)候,數(shù)據(jù)就沒有進(jìn)出的成本,但這么做,其實(shí)是犧牲了一些視圖層的編寫效率。
而Angular2中,用的是async這個(gè)pipe來解決這問題,這也是一種比較方便的辦法,在綁定Observable這一點(diǎn)上,跟有了vue-rx之后的Vue是差不多簡(jiǎn)便的。
React體系里面也有對(duì)RxJS的適配,而且還有跟Redux,Mobx對(duì)接的適配,感興趣的可以自行關(guān)注。
從個(gè)人角度出發(fā),vue-rx這次的升級(jí)很好地滿足了我對(duì)復(fù)雜應(yīng)用開發(fā)的需求了。
本文示例代碼參見:這里
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得
這篇文章主要介紹了詳解VUE-地區(qū)選擇器(V-Distpicker)組件使用心得,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05
vue3項(xiàng)目中各個(gè)文件的作用詳細(xì)介紹
在Vue3項(xiàng)目中,通常會(huì)有以下一些常見的目錄和文件,下面這篇文章主要給大家介紹了關(guān)于vue3項(xiàng)目中各個(gè)文件的作用,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-09-09
element-ui封裝一個(gè)Table模板組件的示例
這篇文章主要介紹了element-ui封裝一個(gè)Table模板組件的示例,幫助大家更好的理解和學(xué)習(xí)vue框架的使用,感興趣的朋友可以了解下2021-01-01
vue + canvas實(shí)現(xiàn)涂鴉面板的示例代碼
這篇文章主要給大家介紹了vue + canvas實(shí)現(xiàn)涂鴉面板的示例,文章通過代碼示例介紹的非常詳細(xì),感興趣的小伙伴跟著小編一起來看看吧2023-08-08

