這篇文章是我 19 年的時候寫的,如今已是 20 年了,vue-rx 又作了一些更新,筆者水平也提高了一些,因此準備重構(refactor)一下這篇文章,最主要的緣由仍是由於某次面試面試官問筆者這篇文章,筆者竟然懵了,有興趣的小夥伴能夠在 github 上看到這篇文章歷史版本(俗稱:黑歷史)
筆者寫源碼分析的時只想將核心部分寫下來,因此但願讀者讀以前先掌握 Vue 中下面幾個功能的用法,文章中不會對基礎作太多講解:javascript
Vue.mixin html
Vue.usejava
Vue.directivenode
而且讀這篇文章以前讀者應該使用過 vue-rx
或者 rxjs
開發過一個以上的項目,不然讀這篇文章對讀者的意義不是很大。git
本文對應的代碼倉庫:https://github.com/MonchiLin/...github
vue-rx 代碼倉庫:https://github.com/vuejs/vue-rx面試
impl-yourself-vue-rx 倉庫裏 src
都可使用 vue serve xx.vue
來直接啓動看效果express
注1:本文會從每一個大函數入手,先根據功能來自行實現一個簡單版本,最後逐行(並不)分析源代碼。api
注2:下文中 vm.xx = xx
vm 爲上文中的 組件的實例),這是 vue
中約定俗成的一個術語。
vue 指令的開發
vue 指令如何與 rxjs 結合
學習一個成熟的 vue 指令是如何開發的
從 Readme 咱們能夠看到, vue-rx 在現代 JS 模塊( ES6 module )系統中用法爲
import Vue from 'vue' import VueRx from 'vue-rx' Vue.use(VueRx)
而後, 根據 Vue.use 文檔可知 Vue.use
函數有兩種用法:
這時咱們打開 index.js 能夠看到下面部分,vue-rx
使用的是第 二 種方法
export default function VueRx (Vue) { install(Vue) Vue.mixin(rxMixin) Vue.directive('stream', streamDirective) Vue.prototype.$watchAsObservable = watchAsObservable Vue.prototype.$fromDOMEvent = fromDOMEvent Vue.prototype.$subscribeTo = subscribeTo Vue.prototype.$eventToObservable = eventToObservable Vue.prototype.$createObservableMethod = createObservableMethod Vue.config.optionMergeStrategies.subscriptions = Vue.config.optionMergeStrategies.data } // auto install if (typeof Vue !== 'undefined') { Vue.use(VueRx) }
能夠看到這個文件默認導出一個函數, 而這個函數接受一個 Vue , 這正是咱們上面提到的 Vue.use 第二種使用形式.
而後咱們看最後兩行
if (typeof Vue !== 'undefined') { Vue.use(VueRx) }
若是 Vue
不是 undefined
那麼就意味着 Vue
是全局變量, 這也就意味着 這裏的 Vue 是經過傳統的 script 標籤引入的,而後調用全局變量的 Vue.use, 而且傳入 VueRx
,這也是默認導出 (export default) 的函數爲何起名爲VueRx
的緣由,
若是執行到這段代碼的 if 語句裏面, 則表示這裏的 Vue
是經過傳統的 script 標籤引入的, 這樣 vue-rx 就能夠兼容 現代 JS 模塊系統 和 經過 script 標籤引入。
接下來咱們來看 VueRx 函數作了什麼。
export default function VueRx (Vue) { install(Vue) Vue.mixin(rxMixin) Vue.directive('stream', streamDirective) Vue.prototype.$watchAsObservable = watchAsObservable Vue.prototype.$fromDOMEvent = fromDOMEvent Vue.prototype.$subscribeTo = subscribeTo Vue.prototype.$eventToObservable = eventToObservable Vue.prototype.$createObservableMethod = createObservableMethod Vue.config.optionMergeStrategies.subscriptions = Vue.config.optionMergeStrategies.data }
export let Vue export let warn = function () {} // NOTE(benlesh): the value of this method seems dubious now, but I'm not sure // if this is a Vue convention I'm just not familiar with. Perhaps it would // be better to just import and use Vue directly? export function install (_Vue) { Vue = _Vue warn = Vue.util.warn || warn }
install()
方法爲這個文件導出的 Vue 和 warn 對象賦值。
註釋的意思是說:這種行爲彷佛是 Vue 的一種約定, 可是卻不必定好, 或許應該使用 import vue
來代替。
咱們看到, 這個文件 默認導出 了一個對象, 而這個對象擁有兩個 vue 的生命週期函數, 這兩個函數最終都會被混入 (mixin) 到全部 vue 的實例中.
先來講說這段代碼實現了什麼效果吧:
容許經過 domStreams 傳入一個字符串數組,而後以字符串的名稱爲變量名建立一個對應的 Subject
,而後將這個 Subject
掛載在組件實例上(經過 this.subjectName) 訪問。
new Vue({ domStreams: ['plus$'] }) // 等價於 vm.plus$ = new Subject()
容許經過 observableMethods 傳入一個字符串數組,而後以字符串的名稱爲變量名建立一個對應的 Observable
。
new Vue({ observableMethods: ['plus$'] }) // 等價於 vm.plus$ = new Observable() // 下面是源代碼,咱們能夠看出源碼作的事情就是在 vm 上掛載開發者傳進來的屬性,值爲 vm.$createObservableMethod 建立的,重點主要在 $createObservableMethod,因此筆者會在 $createObservableMethod 來寫出它的實現和源碼分析,這裏就不贅述了 // 儲存傳入的 observableMethods const observableMethods = vm.$options.observableMethods if (observableMethods) { if (Array.isArray(observableMethods)) { observableMethods.forEach(methodName => { vm[methodName + '$'] = vm.$createObservableMethod(methodName) }) } else { Object.keys(observableMethods).forEach(methodName => { vm[observableMethods[methodName]] = vm.$createObservableMethod(methodName) }) } }
容許經過 subscriptions 傳入一個對象或者一個返回對象的函數,這裏先理解成傳入一個對象便可,而後把對象的屬性掛載在 vm 上面,例以下方代碼例子,便可以用經過 this.count
訪問,對象的屬性的值掛載在 vm.$observables
上面,方便開發者手動訪問。
注意,對象屬性的值應該爲 Observer
相似下面這種結構:
new Vue({ subscriptions: { count: new Subject() .pipp(scan(total, change) => total + change) } }) // 等價於 // 在組件實例上掛載 count 這個屬性 vm.count = undefined // 在 subscriptions.count 對應的值觸發更新時將值賦值給 vm.count const obs = new Subject() .pipp(scan(total, change) => total + change) obs.subscribe(val => vm.count = val) vm.$observables = {} // 保存 observable vm.$observables["count"] = obs
自動取消訂閱,這個功能是和上面那條(第三條)對應的,這段代碼量比較少,就不單獨在 實現 部分寫了。
const subscription = new Subject() .pipp(scan(total, change) => total + change) .subscribe(val => vm.count = val) // subscribe 方法返回一個 subscription 對象,這個對象用於取消訂閱。 // 跟着這個思路,如果要實現自動取消訂閱就須要一個儲存 subscription 對象集的地方 // 例如咱們掛載一個 Subscription 對象放在 vm 上 vm._subscriptions = new Subscription() // 接着利用 Subscription.add 實例方法,可讓一個 Subscription 對象儲存多個 Subscription 對象 vm._subscriptions.add(subscription) // 最後只須要 Subscription.unsubscribe() 便可 vm._subscriptions.unsubscribe()
domStreams
實現的代碼已經在下面了,註釋應該也足夠完善,若是還有疑問請留言。
筆者將寫好的代碼放在了代碼倉庫裏,若是小夥伴們本身寫的時候發現有問題能夠參考這個文件,包含了實現和使用的例子,文件地址
export default { // 在 domStreams: ["plus$", "minus$"], mixins: [ { created() { const vm = this // 獲取 domStreams,此處的 domStreams 值爲 ["plus$", "minus$"] const domStreams = vm.$options.domStreams domStreams.forEach(key => { // 遍歷 domStreams,將 vm[key] 賦值爲 new Subject vm[key] = new Subject() }) // 執行完上面這段 vm["plus$"] 和 vm["minus$"] 的值就變成了 new Subject } } ], }
subscriptions
實現的代碼已經在下面了,註釋應該也足夠完善,若是還有疑問請留言。
筆者將寫好的代碼放在了代碼倉庫裏,若是小夥伴們本身寫的時候發現有問題能夠參考這個文件,包含了實現和使用的例子,文件地址
<template> <div> <button @click="plus$.next($event)">增長</button> <span> >> {{ counter }} << </span> </div> </template> <script> import { map, scan, startWith } from "rxjs/operators"; import { Subject } from "rxjs"; import Vue from 'vue' const plus$ = new Subject() export default { data() { return { plus$: plus$ } }, // 經過 subscriptions 傳入 [counter] subscriptions: { counter: plus$ .pipe( map(() => 1), startWith(0), scan((total, change) => total + change) ) }, mixins: [{ created() { const vm = this // 獲取 subscriptions, 這裏咱們假設 subscriptions = [{counter: Observable}] const subscriptions = vm.$options.subscriptions vm.$observables = {} Object.keys(subscriptions) .forEach(key => { // 在 vm 上建立一個響應式的屬性,這裏等價於 Vue.util.defineReactive(vm, "counter", undefined) Vue.util.defineReactive(vm, key, undefined) vm.$observables[key] = subscriptions[key] // 而後咱們訂閱 Observable, 而且在發生更新的時候賦值給 vm["counter"] subscriptions[key] .subscribe(e => { vm[key] = e }) }) } }], } </script>
export default { created() { const vm = this // 處理 domStreams, 參考上方文章 const domStreams = vm.$options.domStreams // 作空值處理 if (domStreams) { domStreams.forEach(key => { vm[key] = new Subject() }) } const observableMethods = vm.$options.observableMethods if (observableMethods) { if (Array.isArray(observableMethods)) { observableMethods.forEach(methodName => { vm[methodName + '$'] = vm.$createObservableMethod(methodName) }) } else { Object.keys(observableMethods).forEach(methodName => { vm[observableMethods[methodName]] = vm.$createObservableMethod(methodName) }) } } let obs = vm.$options.subscriptions // 這裏判斷 obs 若是是函數就先執行下 if (typeof obs === 'function') { obs = obs.call(vm) } // 作空值處理 if (obs) { // 聲明 $observables 用於儲存 observable vm.$observables = {} // 聲明 _subscription 用於儲存 subscription 以後方便取消訂閱 vm._subscription = new Subscription() Object.keys(obs).forEach(key => { // 使值變成響應式 defineReactive(vm, key, undefined) // 在 $observables 存一份 const ob = vm.$observables[key] = obs[key] // 處理 ob 不是 observable 的狀況 if (!isObservable(ob)) { warn( 'Invalid Observable found in subscriptions option with key "' + key + '".', vm ) return } // 保存 subscription vm._subscription.add(obs[key].subscribe(value => { // 在每次流更新時將值賦給 vm[key] vm[key] = value }, (error) => { throw error })) }) } }, // 在組件的 beforeDestroy 取消訂閱 beforeDestroy() { if (this._subscription) { this._subscription.unsubscribe() } } }
先來看一下用法
<button v-stream:click="plus$">+</button>
格式:v-stream + 事件名(click) = Subject(plus$)
v-stream
實現的代碼已經在下面了,註釋應該也足夠完善,若是還有疑問請留言。
筆者將寫好的代碼放在了代碼倉庫裏,若是小夥伴們本身寫的時候發現有問題能夠參考這個文件,包含了實現和使用的例子
根據基礎用法實現
// <button v-stream:click="plus$">+</button> Vue.directive('stream', { bind: function(el, binding, vNode, oldVnode) { // 傳入的 subject const subject = binding.value // 事件名稱 const eventName = binding.arg // 建立一個 Subscription el._subscription = new Subscription() // 保存 subscibe 返回的 subscription el._subscription.add( fromEvent(el, eventName) .subscribe(e => subject.next(e)) ) }, unbind(el, binding) { // 避免內存泄漏,unbind 生命週期取消訂閱 el._subscription.unsubscribe() } })
好,如今已經實現了最基礎的功能,讓咱們繼續實現第二種用法
// <button v-stream:click="{ subject: plus$, data: someData }">+</button> export default { directives: { stream: { bind: function(el, binding, vNode, oldVnode) { // handle = { subject, data } let handle = binding.value const eventName = binding.arg // 放在這裏方便讀者看,這個函數是以鴨子類型的思想來判斷對象是否爲 observer function isObserver(subject) { return subject && ( typeof subject.next === 'function' ) } // 處理經過 v-stream="plus$" 傳入進來的 subject if (isObserver(handle)) { // 包裝一層,將 v-stream="plus$" 和 // v-stream="{ subject: plus$, data: someData }" 兩種數據結構統一處理 // 此時 handle 數據結構變成了 { subject, data } handle = { subject: handle } } // 還記得 rxMixin 的功能之一嗎? 沒錯就是在 vm 上面掛載 _subscription 對象 // 這裏咱們單獨寫主要是爲了實現 v-stream el._subscription = new Subscription() const subject = handle.subject // 儲存 subscription 對象 el._subscription.add( // 將 event 和 data 都傳遞給開發者 fromEvent(el, eventName) .subscribe(e => subject.next({ event: e, data: handle.data + new Date().getTime() })) ) }, unbind(el, binding) { // 取消訂閱 el._subscription.unsubscribe() } } } }
第三種用法,傳入額外的 option
給原生的 addEventListener
<button v-stream:click="{ subject: plus$, data: someData, options: { once: true, passive: true, capture: true } }">+</button>
康過上面代碼的小夥伴都知道了,咱們使用的是 rxjs
提供的fromEvent
函數,這個函數自己就是依賴 addEventListener
的,從 文檔 的 Parameters
(參數) 部分就能夠看到,若是傳入了三個及以上的參數就會把第三個參數直接傳遞給 addEventListener
,下面咱們作一些小的改造來讓 v-stream
支持這個特性
// 若是 handle 存在 options 則將 options 傳給 fromEvent const fromEventArgs = handle.options ? [el, eventName, handle.options] : [el, eventName] // 儲存 subscription 對象 el._subscription.add( fromEvent(...fromEventArgs) // 將 event 和 data 都傳遞給開發者 .subscribe(e => subject.next({ event: e, data: handle.data + new Date().getTime() })) )
好,如今咱們已經實現了本身的 v-stream
,基於上面的核心理念,咱們開始看 vue-rx
是如何實現 v-stream
的 (注意打開源碼哦:streamDirective 源碼)
import { isObserver, warn, getKey } from '../util' import { fromEvent } from 'rxjs' export default { bind(el, binding, vnode) { let handle = binding.value const event = binding.arg const streamName = binding.expression // 儲存修飾符 const modifiers = binding.modifiers if (isObserver(handle)) { handle = { subject: handle } } else if (!handle || !isObserver(handle.subject)) { // 處理傳入錯誤的參數 warn( 'Invalid Subject found in directive with key "' + streamName + '".' + streamName + ' should be an instance of Subject or have the ' + 'type { subject: Subject, data: any }.', vnode.context ) return } // 定義了一個包含 stop 和 prevent 的處理函數,用於處理 v-stram.stop.prevent="plus$" 的狀況 const modifiersFuncs = { stop: e => e.stopPropagation(), prevent: e => e.preventDefault() } // 接上面, 若是定義的對象里正好包含了 經過指令傳入的 事件屬性, 那麼就將這個屬性儲存到一個新對象 // 方便以後對使用者傳入的事件選項作處理 var modifiersExists = Object.keys(modifiersFuncs).filter( key => modifiers[key] ) const subject = handle.subject // 這裏首先判斷 .next 函數是否存在, 若是存在就用 .next 函數, 若是不存在就用 .onNext 函數 // 這裏是一種兼容性作法, onNext 是 rxjs 好久好久以前的 next 函數, 在 rxjs6 已經被廢棄了 // 因此這裏能夠無視, 咱們繼續往下看, 拿到函數後, 使用 bind 將 this 綁定到 subject, 防止以後使用了錯誤的 this. const next = (subject.next || subject.onNext).bind(subject) // 若是使用 v-stram.native="plus$" 去監聽事件,則會經過 $eventToObservable // 轉換成原生事件,參考 $eventToObservable 源碼解析部分 // vnode.componentInstance 是使用了這個指令的組件實例,也能夠經過 vnode.context 獲取 if (!modifiers.native && vnode.componentInstance) { // 將 subscription 對象儲存在 handle 對象上 // 注意,這裏與筆者的實現是有區別的 handle.subscription = vnode.componentInstance.$eventToObservable(event).subscribe(e => { // 處理事件冒泡和默認行爲 modifiersExists.forEach(mod => modifiersFuncs[mod](e)) next({ event: e, data: handle.data }) }) } else { const fromEventArgs = handle.options ? [el, event, handle.options] : [el, event] handle.subscription = fromEvent(...fromEventArgs).subscribe(e => { modifiersExists.forEach(mod => modifiersFuncs[mod](e)) next({ event: e, data: handle.data }) }) } // 源碼這裏是沒有的,單獨伶出來給讀者看 function getKey(binding) { // 將 事件名稱(binding.arg) 和 修飾符(.native .stop)轉換成字符串 // 例如 v-stream:click.native="plus$" 會轉換成 "click:native" // v-stream:click="plus$" 會轉換成 "click" return [binding.arg].concat(Object.keys(binding.modifiers)).join(':') } // 最後將其儲存在 _rxHandles 中 // store handle on element with a unique key for identifying // multiple v-stream directives on the same node ; (el._rxHandles || (el._rxHandles = {}))[getKey(binding)] = handle } // 觸發指令的 update 生命週期 update(el, binding) { const handle = binding.value const _handle = el._rxHandles && el._rxHandles[getKey(binding)] if (_handle && handle && isObserver(handle.subject)) { // 更新 data,給不記得的小夥伴提個醒,用於更新下面這種方式傳進來的 data // <button v-stream:click="{ subject: plus$, data: someData }">+</button> _handle.data = handle.data } }, // 取消訂閱 unbind(el, binding) { const key = getKey(binding) const handle = el._rxHandles && el._rxHandles[key] if (handle) { if (handle.subscription) { handle.subscription.unsubscribe() } el._rxHandles[key] = null } } }
源碼分析:
import { Observable, Subscription } from 'rxjs' export default function watchAsObservable (expOrFn, options) { const vm = this // 建立一個新的 Observable const obs$ = new Observable(observer => { // 聲明一個變量用於取消 watch, 這裏還未賦值 let _unwatch // 封裝原生的 $watch 函數 const watch = () => { // 參考文檔用法: https://cn.vuejs.org/v2/api/#vm-watch // $watch 函數會返回一個用於取消 watch 的函數 _unwatch = vm.$watch(expOrFn, (newValue, oldValue) => { // watch 方法回調時, 調用 observer.next observer.next({ oldValue: oldValue, newValue: newValue }) }, options) } // 這裏設計的很巧妙, vm._data 實際上就是 $data, 也就是你聲明的 data // 咱們都知道 Vue 在 beforeCreated 生命週期是沒法獲取到 data 的 // 這就會致使 $watch 沒法工做, 因此就等到 created 生命週期去執行 watch 函數 // 因而下面使用了 $once 函數, $once 與 $on 功能很像, 可是 $onec 只會執行一次 // 而且這裏使用了 hook:created, 相信聰明的小夥伴從名字就能看出來, 這裏是監聽 created 生命週期 // 這種用法咱們通常不多用到, 是一種 vue 內部的用法, 官方文檔也有提到 // https://cn.vuejs.org/v2/guide/components-edge-cases.html#%E7%A8%8B%E5%BA%8F%E5%8C%96%E7%9A%84%E4%BA%8B%E4%BB%B6%E4%BE%A6%E5%90%AC%E5%99%A8 // if $watchAsObservable is called inside the subscriptions function, // because data hasn't been observed yet, the watcher will not work. // in that case, wait until created hook to watch. if (vm._data) { watch() } else { vm.$once('hook:created', watch) } // 最後返回一個函數用於取消 watch // 注意這裏的 _unwatch && _unwatch(), 這意味着若是 _unwatch 爲被賦值就不會執行 _unwatch() // 這裏插播一個 rxjs 小知識點, 下面是 observer 部分的返回類型 TeardownLogic 的簽名 // export type TeardownLogic = Unsubscribable | Function | void; // Unsubscribable: 一個帶有 unsubscribe 方法的對象 // Function: 任意函數 // void: 無返回值 // 若是你在 new Observable 的 observer 參數部分手動返回一個 Subscription 對象, 那麼調用 // 這個 Observable.subscribe 方法返回的 Subscription 對象的 unsubscribe 方法時將會執行你返回執行你傳入的函數 // 若是你在 new Observable 的 observer 參數部分手動返回一個函數, 那麼調用 // 這個 Observable.subscribe 方法返回的 Subscription 對象的 unsubscribe 方法時將會執行你返回的函數 // Returns function which disconnects the $watch expression return new Subscription(() => { _unwatch && _unwatch() }) }) // 而後咱們把這個 Observable 返回出去, 就能夠實現官方用法中的效果了. return obs$ }
源碼解析:
import { Observable, Subscription, NEVER } from 'rxjs' export default function fromDOMEvent (selector, event) { // 處理 window 不存在的狀況 if (typeof window === 'undefined') { // TODO(benlesh): I'm not sure if this is really what you want here, // but it's equivalent to what you were doing. You might want EMPTY return NEVER } const vm = this // 獲取 dom 的根元素, 也就是咱們寫下面這段代碼中的 <html></html> // <html> // <body></body> // </html> // doc 變量的做用, 也就是這個函數的工做原理, 爲何上面介紹說這個函數能夠在 DOM 渲染前生效 // 由於事件監聽的 DOM 是 html, 就意味着不管什麼時候只要你觸發這個事件都會執行傳入的 處理事件(下面的 listener) // 能夠看出這是一個全局的監聽事件, 上面介紹有提到 fromDOMEvent 函數只會影響到當前組件的內部元素, 如何只影響組件內部 // 就要看下面的 listener 函數了 const doc = document.documentElement // 建立一個 Observable const obs$ = new Observable(observer => { function listener (e) { // 首先判斷, 若是 vm.$el 不存在就退出函數執行 // 由於 vm.$el 不存在就由於這 vm 已經不被掛載在 dom 上了 if (!vm.$el) return // 這裏是處理 selector 參數爲 null 的狀況, 若是 selector 爲 null 而且當前事件的 target 是當前組件實例的 dom // 就把事件對象傳入 next 方法 if (selector === null && vm.$el === e.target) return observer.next(e) // 這裏從當前組件實例中去 querySelectorAll, 這意味着 selector 只能從當前組件實例中匹配 var els = vm.$el.querySelectorAll(selector) // 取出事件的 target var el = e.target // 循環上面 querySelectorAll 匹配到的 dom for (var i = 0, len = els.length; i < len; i++) { // 若是 dom 匹配則把事件對象傳入 next 方法 if (els[i] === el) return observer.next(e) } } // 將上面的 listener 做爲事件處理函數傳入 addEventListener doc.addEventListener(event, listener) // 參考 watchAsObservable 對相似代碼的解釋 // Returns function which disconnects the $watch expression return new Subscription(() => { doc.removeEventListener(event, listener) }) }) return obs$ }
源碼解讀:
import { Subscription } from 'rxjs' // 還記得 rxMixin 中的 _subscription 嗎?沒錯,又是它,咱們使用 vm.$subscribeTo 進行訂閱的時候 // 返回的 subscription 對象會被添加到 vm._subscription 從而實現自動取消訂閱 export default function subscribeTo (observable, next, error, complete) { // 調用傳入的 observable, 以及其參數, 獲得 subscription const subscription = observable.subscribe(next, error, complete) // 這裏代碼避開上去複雜了一點, 其實它的目的就是儲存這個 subscription, 而後在某個時機取消訂閱 // 這裏以 ; 開頭實際上是由於 js 解析器解析括號時是不會加分號的, 這也就致使出現下面的狀況 const subscription = observable.subscribe(next, error, complete)(this._subscription || (this._subscription = new Subscription())).add(subscription) // 對, 就像上面這樣, 將兩段代碼連在了一塊兒, 這個 bug 現代解析器已經修復了, 這裏特意提一下也是防止小夥伴們踩坑(纔不是強行解釋) // ok, 如今來講代碼內容, 首先判斷 this._subscription 是否存在, 若是不存在就建立一個 _subscription // 而後將上面的 subscription 對象傳入 ;(this._subscription || (this._subscription = new Subscription())).add(subscription) return subscription }
源碼解讀:
import { Observable } from 'rxjs' export default function eventToObservable (evtName) { const vm = this // 雖然上面的官方用法中沒有提到, 但咱們看源碼能夠發現這個參數也能夠接受一個數組, 若是發現 // 接受的參數不是一個數組, 那麼就將其轉換成一個數組 const evtNames = Array.isArray(evtName) ? evtName : [evtName] // 建立 Observable 對象 const obs$ = new Observable(observer => { // 儲存用於取消監聽事件的對象 const eventPairs = evtNames.map(name => { // 生成 callback const callback = msg => observer.next({ name, msg }) // 做爲參數傳入 vm.$on, 這樣組件就能夠自動監聽了 vm.$on(name, callback) return { name, callback } }) return () => { // 取消監聽事件 eventPairs.forEach(pair => vm.$off(pair.name, pair.callback)) } }) return obs$ }
功能介紹:
你可使用 observableMethods
選項使代碼更加聲明式:
new Vue({ observableMethods: { submitHandler: 'submitHandler$' // 或者使用數組簡寫: ['submitHandler'] } });
上面代碼會自動在實例上建立兩個東西:
v-on
綁定到模板的 submitHandler
方法;submitHandler
的submitHandler$
observable。筆者將寫好的代碼放在了代碼倉庫裏,若是小夥伴們本身寫的時候發現有問題能夠參考這個文件,包含了實現和使用的例子,文件地址
export default { mixins: [{ created() { const vm = this const $createObservableMethod = (methodName) => { // 定義一個 subscriber const subscriber = (observer) => { // 在 vm 上掛載方法 vm["muchMore"] = xx // 調用這個方法時就會觸發 observer.next vm[methodName] = (val) => { observer.next(val) } return () => { delete vm[methodName] } } return new Observable(subscriber).pipe(share()) } // 假設咱們傳入以下結構 // observableMethods: { // muchMore: 'muchMore$', // minus: 'minus$' // } const observableMethods = vm.$options.observableMethods Object.keys(observableMethods).forEach(key => { // 在 vm 上面掛載 muchMore$,muchMore$ 是一個 Observable // vm["muchMore$"] = $createObservableMethod("muchMore") vm[observableMethods[key]] = $createObservableMethod(key) }) } }], }
export default function createObservableMethod (methodName, passContext) { const vm = this // 處理錯誤 if (vm[methodName] !== undefined) { warn( 'Potential bug: ' + `Method ${methodName} already defined on vm and has been overwritten by $createObservableMethod.` + String(vm[methodName]), vm ) } const creator = function (observer) { vm[methodName] = function () { // arguments 是 function 聲明的函數的特有屬性,由實參構成的「類數組」 // Array.from(arguments) 將 arguments 轉換成數組 const args = Array.from(arguments) if (passContext) { args.push(this) observer.next(args) } else { if (args.length <= 1) { observer.next(args[0]) } else { observer.next(args) } } } // 取消訂閱時刪除 vm 上的方法 return function () { delete vm[methodName] } } return new Observable(creator).pipe(share()) }
解析:
Vue.config.optionMergeStrategies.subscriptions = Vue.config.optionMergeStrategies.data
咱們看到 subscriptions 的合併策略被賦值了 data 的合併策略, 這意味着咱們只須要搞明白 data 的合併策略就知道 subscriptions 的合併策略了,想了解這部分知識的小夥伴能夠能夠去讀 Vue 源碼,若是須要的話筆者也能夠單獨寫一篇文章來說解。
源碼在 vue 倉庫的 vue/src/core/util/options.js 110行。