Vue2.0源碼閱讀筆記(十一):自定義事件

  Vue 事件分爲兩類:原生DOM事件、自定義事件。其中原生DOM事件既能夠在元素上使用,也能夠在組件上使用,在組件上使用時要添加.native修飾符。
  Vue 經過調用原生API來處理元素和組件上綁定的原生DOM事件,在組件上的自定義事件則是由基於發佈/訂閱模式的事件中心機制完成的。
前端

1、手動實現事件中心

  手動實現一個遵循發佈/訂閱模式的事件中心比較簡單,代碼以下所示:
vue

function EventEmitter() {
  this._events = Object.create(null)
}

EventEmitter.prototype.$on = function(type, handler) {
  (this._events[type] || (this._events[type] = [])).push(handler)
}

EventEmitter.prototype.$off = function(type, handler) {
  var events = this._events[type]
  if (events) {
    var cb
    var i = events.length
    while (i--) {
      cb = events[i]
      if (cb === handler) {
        events.splice(i, 1)
        break
      }
    }
  }
}

EventEmitter.prototype.$emit = function(type) {
  var args = toArray(arguments, 1)
  if (this._events[type]) {
    this._events[type].forEach(fn => fn.apply(this, args))
  }
}

EventEmitter.prototype.$once = function(type, handler) {
  var _this = this
  var flag = false

  function on() {
    _this.$off(type, on)
    if (!flag) {
      flag = true
      handler.apply(_this, arguments)
    }
  }

  _this.$on(type, on)
}

export default EventEmitter
複製代碼

  訂閱方法都保存在實例對象的 _events 屬性中,自定義事件的名稱爲 _events 的屬性,其值爲數組類型,存儲着事件觸發後的回調函數。
  經過調用 $on 方法來訂閱事件,實現方式是將回調函數推入 _events 對象上的對應屬性數組中。
  經過調用 $off 方法來移除自定義事件監聽器,實現方式是將回調函數從 _events 對象上的對應屬性數組中刪除。
  經過調用 $emit 方法來觸發對應事件,本質上是將對應訂閱方法取出執行。
  經過調用 $once 方法來添加一個只觸發一次的自定義事件,實現過程當中利用一個標識變量來判斷方法是否觸發。
vuex

2、Vue事件中心

  在上篇文章《指令》講到 v-on 指令時,關於在組件上使用自定義指令的部分沒有闡述其具體實現。僅僅說到自定義事件的添加與刪除最終是調用了實例方法 $on 與 $off 來完成的,自定義事件的觸發是調用實例方法 $emit 來完成。
  在實例化Vue對象時會調用其構造函數:
數組

function Vue (options) {
  /* 省略警告信息 */
  this._init(options)
}
/* 省略其它mixin */
eventsMixin(Vue)

Vue.prototype._init = function (options) {
  const vm = this
  /* 省略... */
  initEvents(vm)
}
複製代碼

  儲存訂閱方法的對象在 initEvents 方法中定義,實例事件方法在 eventsMixin 中定義。
app

function initEvents (vm) {
  vm._events = Object.create(null)
  /* 省略... */
}

function eventsMixin (Vue) {
  const hookRE = /^hook:/
  Vue.prototype.$on=function(event,fn){/*省略*/}

  Vue.prototype.$once=function(event,fn){/*省略*/}

  Vue.prototype.$off=function(event,fn){/*省略*/}

  Vue.prototype.$emit=function(event){/*省略*/}
}
複製代碼

  首先來看 $on 的具體實現:
函數

Vue.prototype.$on = function(event,fn){
  const vm = this
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn)
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn)
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}
複製代碼

  能夠看到 Vue 源碼中的 $on 跟咱們上一節手動實現的核心代碼是一致的,只是 Vue 的 $on 方法第一個參數能夠爲數組,所以在函數開始進行這種狀況的處理:若是是數組,則循環調用 $on 方法,以數組中的值爲第一個參數。
  若是第一個參數 event 與 /^hook:/ 正則匹配時,將實例對象的 _hasHookEvent 屬性置爲 true,這跟生命週期鉤子函數有關,相關細節將在下一篇文章《生命週期》中闡述。
post

Vue.prototype.$off=function(event,fn){
  const vm = this

  if (!arguments.length) {
    vm._events = Object.create(null)
    return vm
  }

  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn)
    }
    return vm
  }

  const cbs = vm._events[event]
  if (!cbs) { return vm }
  if (!fn) {
    vm._events[event] = null
    return vm
  }

  let cb
  let i = cbs.length
  while (i--) {
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1)
      break
    }
  }
  return vm
}
複製代碼

  在 $off 方法中對了許多邊界條件的處理,好比不傳任何參數調用該方法則將 vm._events 變量置空,即移除全部的事件監聽器;若是第一個參數是數組則循環調用 $off 方法;若是隻提供第一個參數,則將 vm._events[event] 置空,即移除該事件全部的監聽器。
  能夠看到,$off 方法的核心實現中比咱們手動實現的多了一個條件:cb.fn === fn。事件的 fn 屬性與要刪除的方法相同也執行刪除操做,之因此加上這一個條件是爲了配合 $once 方法的實現。
學習

Vue.prototype.$once=function(event, fn){
  const vm = this
  function on () {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  on.fn = fn
  vm.$on(event, on)
  return vm
}
複製代碼

  $once 方法是經過調用 $on 方法實現的,只是將回調包裹在內部函數 on 中,在觸發 $on 方法夠會調用內部函數中的 $off 方法移除該事件監聽,從而實現了 $once 方法監聽一個自定義事件,可是隻觸發一次的功能。
ui

Vue.prototype.$emit=function(event){
  const vm = this
  /* 省略警告信息 */
  let cbs = vm._events[event]
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    const info = `event handler for "${event}"`
    for (let i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    }
  }
  return vm
}
複製代碼

  $emit 方法的實現與上一節手動實現的原理同樣,就是執行存儲在 _events 中的對應方法,只是 Vue 經過調用 invokeWithErrorHandling 進行了一些錯誤處理。
this

3、EventBus

  EventBus 即爲事件總線,能夠很方便的實現非父子組件間通訊。EventBus 在 Vue 中的具體實現就是經過事件中心機制實現的。

// event-bus.js
import Vue from 'vue'
const bus = new Vue()
export default bus

// A 組件
import bus from '@/event-bus.js'
bus.$on('CONSOLE', number => {
  console.log(number)
})

// B 組件
import bus from '@/event-bus.js'
bus.$emit('CONSOLE', 1)

// C 組件
import bus from '@/event-bus.js'
bus.$off('CONSOLE')
複製代碼

  EventBus 實現簡單,操做便捷,具備很高的靈活性。但就是由於過於靈活,若是在項目中隨意使用,那後期維護起來將是災難。
  若是有不少地方須要進行非父子組件間的通訊,最正確的選擇是使用 vuex 進行狀態管理,關於 vuex 的原理闡述將在後續文章進行介紹。

4、總結

  Vue 中對自定義事件的處理是經過基於發佈/訂閱模式的事件中心機制實現的,核心思路就是將事件存儲到實例的一個屬性對象上,事件的添加、刪除、觸發等操做都是在該對象上進行的。

歡迎關注公衆號:前端桃花源,互相交流學習!

相關文章
相關標籤/搜索