Vue源码阅读(23):vm.$on、vm.$off、vm.$once、vm.$emit 源码解析

本想写 v-on 指令的源码解析,但是 v-on 指令的底层会使用到标题中的四个实例方法,所以,今天先把这四个与事件有关的实例方法讲清楚。

知识补充:上面说的实例是指 Vue 实例,在 Vue 应用中有两种类型的 Vue 实例,第一种是通过 new Vue({xxx}) 手动创建的 Vue 实例。第二种是组件 Vue 实例,当我们在模板中使用组件的时候,Vue 会为使用的每个组件创建对应的 Vue 实例。

先看下这四个方法是如何绑定到 Vue 实例上的。

src/core/instance/index.js。在该文件中,创建了 Vue 函数,然后以 Vue 函数为参数执行 eventsMixin() 方法。

import { eventsMixin } from './events'

function Vue (options) {
  this._init(options)
}

eventsMixin(Vue)

export default Vue

src/core/instance/events.js。在 eventsMixin 方法中,向 Vue 的原型对象上写入四个原型方法,分别是:$on、$once、$off、$emit,这四个方法都在 Vue 函数的原型对象中,所以通过 Vue 函数实例化创建出来的 Vue 实例能够使用这四个方法。

export function eventsMixin (Vue: Class) {
  Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
    ......
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    ......
  }

  Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
    ......
  }

  Vue.prototype.$emit = function (event: string): Component {
    ......
  }
}

 接下来开始源码的解析。

1,vm.$on

首先看下 vm.$on 的官方文档。

Vue源码阅读(23):vm.$on、vm.$off、vm.$once、vm.$emit 源码解析_第1张图片

该实例方法内部的实现很简单,只需要将通过 $on 注册的事件和回调函数保存起来,在通过 vm.$emit() 触发事件的时候,将保存起来的对应回调函数取出来执行即可,我们直接看源码,解释看源码中的注释即可。

Vue.prototype.$on = function (event: string | Array, fn: Function): Component {
  const vm: Component = this
  // 判断注册的 event 是不是数组,如果是数组的话,则数组中的多个事件都需要注册 fn 函数
  if (Array.isArray(event)) {
    // 遍历事件数组
    for (let i = 0, l = event.length; i < l; i++) {
      // 针对当前遍历的事件,注册 fn 函数
      this.$on(event[i], fn)
    }
  } else {
    // vm._events 是保存事件及其回调函数的地方,数据结构如下所示:
    // {
    //   eventName1: [fn1, fn2, fn3, fn4],
    //   eventName2: [fn1, fn4, fn5],
    //   ......
    // }
    // vm._events 是一个对象,对象的 key 是注册进来的事件名,对象的 value 是数组,数组中保存着事件所对应的回调函数
    // 如果 vm._events[event] 不存在的话,则使用空数组初始化,然后使用 push 将 fn 函数添加到事件列表中。
    (vm._events[event] || (vm._events[event] = [])).push(fn)
  }
  return vm
}

2,vm.$off

首先看下 vm.$off 的官方文档。

Vue源码阅读(23):vm.$on、vm.$off、vm.$once、vm.$emit 源码解析_第2张图片

内部的实现原理是:根据传递的事件和回调函数到 vm._events 中删除注册的事件或者回调函数,源码和注释如下所示。

Vue.prototype.$off = function (event?: string | Array, fn?: Function): Component {
  const vm: Component = this
  // 情况1:如果没有传递参数的话,则移除所有注册的事件和回调函数
  if (!arguments.length) {
    // 移除的方式是创建一个空的对象赋值到 vm._events
    vm._events = Object.create(null)
    return vm
  }
  // 如果 event 是数组的话,则遍历 event 数组,并依次调用 this.$off(event[i], fn)
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$off(event[i], fn)
    }
    return vm
  }
  // 在这里处理单个的 event 事件,通过 vm._events[event] 获取 event 事件的回调函数数组
  const cbs = vm._events[event]
  // 如果 cbs 不存在的话,说明没有注册 event 事件,直接 return 返回即可
  if (!cbs) {
    return vm
  }
  // 情况2:判断是不是只有一个参数,如果是的话,则情形是:只提供了事件,没有提供回调函数
  if (arguments.length === 1) {
    // 此时将 vm._events 中的 event 事件数组设置为 null 即可
    vm._events[event] = null
    return vm
  }
  // 情况3:在这里判断有没有提供回调函数,如果提供了的话,则情形是:即提供了事件,也提供了回调函数
  // 此时需要移除指定事件的指定回调函数
  if (fn) {
    // specific handler
    let cb
    let i = cbs.length
    // 遍历回调函数数组 cbs
    // 这里有个细节是:遍历的方向是从后往前,因为这样移除掉某个回调函数的时候,
    // 不会影响前面未处理回调函数的位置。
    while (i--) {
      cb = cbs[i]
      // 如果 cb 等于 fn 或者 cb.fn 等于 fn 的话,说明当前的 cb 就是要移除的回调函数,
      // 至于为什么 cb.fn 等于 fn 也可以,看下面的 vm.$once 的源码解释,这里不做讨论
      // 使用 splice 将其移除掉即可。
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
  }
  return vm
}

3,vm.$once

首先看下 vm.$once 的官方文档。

Vue源码阅读(23):vm.$on、vm.$off、vm.$once、vm.$emit 源码解析_第3张图片

 $once 的源码内容并不多,但是却有些复杂,我们看下面 $once 的源码和注释。

Vue.prototype.$once = function (event: string, fn: Function): Component {
  const vm: Component = this
  // $once 的实现思路是提供一个封装函数,将这个封装函数注册到 vm._events 中,当使用 vm.$emit('event')
  // 触发事件的时候,实际执行的是这个封装函数,然后在封装函数中使用 $off 移除掉封装函数 on,这样
  // 封装函数就不会再被 $emit 触发执行了,移除掉封装函数本身之后,再调用执行真正的业务函数 fn。
  // 这样就可以实现 fn 函数只能被 $emit 触发执行一次的效果了
  function on () {
    vm.$off(event, on)
    fn.apply(vm, arguments)
  }
  // 至于为什么要将 fn 赋值到 on.fn 呢?
  // 这是因为用户使用 $once 注册事件之后,有可能会使用 $off 移除使用 $once 注册的事件,
  // 用户的移除操作是 this.$off('event', fn),但是实际注册的回调函数不是 fn,而是封装函数 on,
  // 所以用户执行的 this.$off('event', fn) 并不能达到预期的效果。
  //
  // Vue 的解决方案是将 fn 赋值到 on.fn,然后在 $off 函数中,判断如果 cb.fn === fn 的话,也
  // 会进行移除的操作。这样,用户执行 this.$off('event', fn) 就能够达到移除 fn 回调的效果了。
  on.fn = fn
  vm.$on(event, on)
  return vm
}

 4,vm.$emit

首先看下 vm.$emit 的官方文档,点击这里。

$emit 是最简单的,只需要使用传递进来的 event 事件名,到 vm._events 中获取 event 事件的回调函数列表,然后遍历执行回调函数即可,源码如下。

Vue.prototype.$emit = function (event: string): Component {
  const vm: Component = this

  // 使用 event 事件名获取回调函数列表
  let cbs = vm._events[event]
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs
    const args = toArray(arguments, 1)
    // 遍历执行 cbs 中的回调函数
    for (let i = 0, l = cbs.length; i < l; i++) {
      try {
        cbs[i].apply(vm, args)
      } catch (e) {
        handleError(e, vm, `event handler for "${event}"`)
      }
    }
  }
  return vm
}

你可能感兴趣的:(vue源码阅读系列,前端,vue.js,源码)