Vue2.0源码阅读笔记(十一):自定义事件

1,020 阅读4分钟

  Vue 事件分为两类:原生DOM事件、自定义事件。其中原生DOM事件既可以在元素上使用,也可以在组件上使用,在组件上使用时要添加.native修饰符。
  Vue 通过调用原生API来处理元素和组件上绑定的原生DOM事件,在组件上的自定义事件则是由基于发布/订阅模式的事件中心机制完成的。

一、手动实现事件中心

  手动实现一个遵循发布/订阅模式的事件中心比较简单,代码如下所示:

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 方法来添加一个只触发一次的自定义事件,实现过程中利用一个标识变量来判断方法是否触发。

二、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 中定义。

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,这跟生命周期钩子函数有关,相关细节将在下一篇文章《生命周期》中阐述。

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 方法监听一个自定义事件,但是只触发一次的功能。

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 进行了一些错误处理。

三、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 的原理阐述将在后续文章进行介绍。

四、总结

  Vue 中对自定义事件的处理是通过基于发布/订阅模式的事件中心机制实现的,核心思路就是将事件存储到实例的一个属性对象上,事件的添加、删除、触发等操作都是在该对象上进行的。

欢迎关注公众号:前端桃花源,互相交流学习!