深刻 VUE-RX 源碼

  • [建立時間]: 2019/2/15
  • [更新時間]: 2019/3/31
  • [keyword]: Vue,Vue Directive,Javasctipt,RxJS,源碼閱讀
這篇文章是我 19 年的時候寫的,如今已是 20 年了,vue-rx 又作了一些更新,筆者水平也提高了一些,因此準備重構(refactor)一下這篇文章,最主要的緣由仍是由於某次面試面試官問筆者這篇文章,筆者竟然懵了,有興趣的小夥伴能夠在 github 上看到這篇文章歷史版本(俗稱:黑歷史)

筆者寫源碼分析的時只想將核心部分寫下來,因此但願讀者讀以前先掌握 Vue 中下面幾個功能的用法,文章中不會對基礎作太多講解:javascript

Vue.mixin html

Vue.$optionsvue

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 指令是如何開發的

從 Vue.use 開始

從 Readme 咱們能夠看到, vue-rx 在現代 JS 模塊( ES6 module )系統中用法爲

import Vue from 'vue'
import VueRx from 'vue-rx'

Vue.use(VueRx)

而後, 根據 Vue.use 文檔可知 Vue.use 函數有兩種用法:

  1. 傳入一個函數, 把 Vue 做爲參數傳入這個函數, 而且調用它.
  2. 傳入一個對象, 會調用傳入對象的 install 函數

這時咱們打開 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 函數作了什麼。

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
}

install(Vue)

源碼位置

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(rxMixin)

源碼位置

咱們看到, 這個文件 默認導出 了一個對象, 而這個對象擁有兩個 vue 的生命週期函數, 這兩個函數最終都會被混入 (mixin) 到全部 vue 的實例中.

概覽

先來講說這段代碼實現了什麼效果吧:

  1. 容許經過 domStreams 傳入一個字符串數組,而後以字符串的名稱爲變量名建立一個對應的 Subject,而後將這個 Subject 掛載在組件實例上(經過 this.subjectName) 訪問。

    new Vue({
      domStreams: ['plus$']
    })
    // 等價於 vm.plus$ = new Subject()
  2. 容許經過 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)
        })
      }
    }
  3. 容許經過 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
  4. 自動取消訂閱,這個功能是和上面那條(第三條)對應的,這段代碼量比較少,就不單獨在 實現 部分寫了。

    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

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

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()
    }
  }
}

Vue.directive('stream', streamDirective)

streamDirective 源碼

v-stream 文檔

先來看一下用法

<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
    }
  }
}

$watchAsObservable

源碼文件

源碼分析:

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$
}

$fromDOMEvent

源碼位置

源碼解析:

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$
}

$subscribeTo

源碼位置

源碼解讀:

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
}

$eventToObservable

源碼位置

源碼解讀:

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$
}

$createObservableMethod

功能介紹:

你可使用 observableMethods 選項使代碼更加聲明式:

new Vue({
  observableMethods: {
    submitHandler: 'submitHandler$'
    // 或者使用數組簡寫: ['submitHandler']
  }
});

上面代碼會自動在實例上建立兩個東西:

  1. 一個是能夠用 v-on 綁定到模板的 submitHandler 方法;
  2. 一個是能夠流式調用 submitHandlersubmitHandler$ 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())
}

optionMergeStrategies.subscriptions

解析:

Vue.config.optionMergeStrategies.subscriptions = Vue.config.optionMergeStrategies.data

咱們看到 subscriptions 的合併策略被賦值了 data 的合併策略, 這意味着咱們只須要搞明白 data 的合併策略就知道 subscriptions 的合併策略了,想了解這部分知識的小夥伴能夠能夠去讀 Vue 源碼,若是須要的話筆者也能夠單獨寫一篇文章來說解。

源碼在 vue 倉庫的 vue/src/core/util/options.js 110行。

相關文章
相關標籤/搜索