写在前面

自定义事件的监听和触发也就是 EventBus 发布订阅模式,在前面 Vue 组件间通信中有介绍到使用 EventBus 通信的方式,就是通过使用 Vue 实例自带的 $on $emit $off $once 方法进行自定义事件的监听和触发,然后将数据传递出去。那么 Vue 的自定义事件机制是如何实现的呢?

其实发布订阅模式的具体实现思路是通用的,因此在了解了 Vue 的自定义事件机制后就了解了发布订阅模式的事件机制了。

1. $on $emit $off $once 的使用

首先要先过一遍这几个事件函数的使用方式,这里直接展示 vue官网 的官方讲解图片,这里注意看可传入的参数类型和代表的含义。讲解中的传入的参数名加 [] 的就是可选参数的意思,不是数组的意思,参数类型看 {} 内的类型。如下所示:
vue自定义事件的 $emit $on $off $once 的实现 - 图1

vue自定义事件的 $emit $on $off $once 的实现 - 图2

vue自定义事件的 $emit $on $off $once 的实现 - 图3

vue自定义事件的 $emit $on $off $once 的实现 - 图4

2. 事件发布订阅的设计思路

  • 首先每一个 Vue 实例对象内部都有一个自己的事件对象,即 vm._events = {},该对象用于存储事件名和事件回调函数的键值对,因为一个事件可以有多个回调函数,因此,事件键值对的值是一个回调函数数组。
  • 每调用一次 $on 函数就是注册一次事件监听,就会往事件对象中添加一个事件键值对,如果事件已经存在,就向事件回调函数数组中 push 一个回调。vm._events = { event1: [cb1, cb2] }
  • 对应的,当调用 $off 函数的时候就是从该事件对应的回调函数数组中 pop 出一个要移除的回调。
  • $emit 函数执行的时候,会将对应事件的回调函数数组遍历一遍然后依次执行。$emit 函数只是会执行对应事件的回调函数数组中的函数,并不会将其从数组移除,因此,$emit 是可以多次触发的。
  • 最后就是 $once 的设计思路了,$once 是事件中相对复杂一点的设计,$once 是注册一个只能执行一次的回调函数。使用 $emit 触发一次后,再次触发就不起作用了。其设计思路就是,如果我们能在这个回调函数执行之后就立即调用 $off 函数将其 pop 出去,就可以实现一次性使用的效果了,这个手动调用 $off 的语句总不能交给使用 Vue 的使用者吧,那就没有 $once 的意义了,然后就是,回调函数是外部传进来了,肯定不能对其里面进行修改,$off 语句也不能放到回到函数本身,因此,Vue 的 $once 函数的设计思路就是,在 $once 函数传进来的回调函数的基础上封装一个高阶函数 on,将这个高阶函数 push 到回调函数数组中,这个高阶函数内部做了两件事,一件是 vm.$off(event, on) 手动取消 on 事件的监听,另一件就是调用 $once 传进来的回调函数。定义好 on 函数后就手动 vm.$on(event, on) 注册封装好的 on 函数,但此时存在一个问题,因为我们在 $once 中注册的事件回调是 on 函数,那当用户使用 $off 手动取消该事件的原始 callback 函数时,就在事件回调数组中找不到该回调函数名了,因此,$once 将原本的回调函数 fn 绑定在了高阶函数 on 上,即 on.fn = fn,在 $on 函数中判断即可。

    3. Vue 源码具体实现(来自下方参考博客)

    $on

    1. Vue.prototype.$on = function (event, fn) {
    2. const vm = this
    3. // 我们传入的要监听的事件可能为数组,这时候对数组里的每个事件再递归调用$on方法
    4. if (Array.isArray(event)) {
    5. for (let i = 0, l = event.length; i < l; i++) {
    6. vm.$on(event[i], fn)
    7. }
    8. } else {
    9. // 之前已经有监听event事件,则将此次监听的回调函数添加到其数组中,否则创建一个新数组并添加fn
    10. (vm._events[event] || (vm._events[event] = [])).push(fn)
    11. }
    12. return vm
    13. }

$off

  1. Vue.prototype.$off = function (event, fn) {
  2. const vm = this
  3. // all
  4. if (!arguments.length) {
  5. // 如果没有传参数,则清空所有事件的监听函数
  6. vm._events = Object.create(null)
  7. return vm
  8. }
  9. // 如果传的event是数组,则对该数组里的每个事件再递归调用$off方法
  10. if (Array.isArray(event)) {
  11. for (let i = 0, l = event.length; i < l; i++) {
  12. vm.$off(event[i], fn)
  13. }
  14. return vm
  15. }
  16. // 获取当前event里所有的回调函数
  17. const cbs = vm._events[event]
  18. // 如果不存在回调函数,则直接返回,因为没有可以移除监听的内容
  19. if (!cbs) {
  20. return vm
  21. }
  22. // 如果没有指定要移除的回调函数,则移除该事件下所有的回调函数
  23. if (!fn) {
  24. vm._events[event] = null
  25. return vm
  26. }
  27. // 指定了要移除的回调函数
  28. let cb
  29. let i = cbs.length
  30. while (i--) {
  31. cb = cbs[i]
  32. // 在事件对应的回调函数数组里面找出要移除的回调函数,并从数组里移除
  33. if (cb === fn || cb.fn === fn) {
  34. cbs.splice(i, 1)
  35. break
  36. }
  37. }
  38. return vm
  39. }

$emit

  1. Vue.prototype.$emit = function (event) {
  2. const vm = this
  3. // 拿出触发事件对应的回调函数列表
  4. let cbs = vm._events[event]
  5. if (cbs) {
  6. // $emit方法可以传参,这些参数会在调用回调函数的时候传进去
  7. const args = toArray(arguments, 1)
  8. // 遍历回调函数列表,调用每个回调函数
  9. for (let i = 0, l = cbs.length; i < l; i++) {
  10. cbs[i].apply(vm, args)
  11. }
  12. }
  13. return vm
  14. }
  15. }

$once

  1. Vue.prototype.$once = function (event, fn) {
  2. const vm = this
  3. // 封装一个高阶函数on,在on里面调用fn
  4. function on () {
  5. // 每当执行了一次on,移除event里的on事件,后面再触发event事件就不会再执行on事件了,也就不会执行on里面的fn事件
  6. vm.$off(event, on)
  7. // 执行on的时候,执行fn函数
  8. fn.apply(vm, arguments)
  9. }
  10. // 这个赋值是在$off方法里会用到的
  11. // 比如我们调用了vm.$off(fn)来移除fn回调函数,然而我们在调用$once的时候,实际执行的是vm.$on(event, on)
  12. // 所以在event的回调函数数组里添加的是on函数,这个时候要移除fn,我们无法在回调函数数组里面找到fn函数移除,只能找到on函数
  13. // 我们可以通过on.fn === fn来判断这种情况,并在回调函数数组里移除on函数
  14. on.fn = fn
  15. // $once最终调用的是$on,并且回调函数是on
  16. vm.$on(event, on)
  17. return vm
  18. }

参考博客

vue源码解析事件派发($on、$emit、$once、$off)
vue events 源码