$emit, $on实现原理剖析,快动手写一个eventBus吧😃

237 阅读2分钟

1、先贴一下源码

1.1核心代码实现

源码位置: src/core/instance/events.js

代码有删减

/* @flow */

import {
  tip,
  toArray,
  hyphenate,
  formatComponentName,
  invokeWithErrorHandling
} from '../util/index'

// ...

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
    const vm: Component = 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)
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) {
        vm._hasHookEvent = true
      }
    }
    return vm
  }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on () {
      vm.$off(event, on)
      fn.apply(vm, arguments)
    }
    on.fn = fn
    vm.$on(event, on)
    return vm
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    const vm: Component = this
    // all
    if (!arguments.length) {
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event]
    if (!cbs) {
      return vm
    }
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // specific handler
    let cb
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (process.env.NODE_ENV !== 'production') {
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
          `${formatComponentName(vm)} but the handler is registered for "${event}". ` +
          `Note that HTML attributes are case-insensitive and you cannot use ` +
          `v-on to listen to camelCase events when using in-DOM templates. ` +
          `You should probably use "${hyphenate(event)}" instead of "${event}".`
        )
      }
    }
    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
  }
}

1.2、invokeWithErrorHandling 方法实现

下面看一下 invokeWithErrorHandling 的实现逻辑

源码位置: src\core\util\error.js

export function invokeWithErrorHandling (
  handler: Function,
  context: any,
  args: null | any[],
  vm: any,
  info: string
) {
  let res
  try {
    res = args ? handler.apply(context, args) : handler.call(context)
    if (res && !res._isVue && isPromise(res) && !res._handled) {
      res.catch(e => handleError(e, vm, info + ` (Promise/async)`))
      // issue #9511
      // avoid catch triggering multiple times when nested calls
      res._handled = true
    }
  } catch (e) {
    handleError(e, vm, info)
  }
  return res
}

1.3、分析

  1. Vue 的数据相应是依赖于“发布-订阅”模式,onon、emit也是基于这种模式

  2. $on 用来收集所有的事件依赖,他会将传入的参数event和fn作为key和value的形式存到vm._events这个事件集合里,就像这样vm._events[event]=[fn]。以便于emit的时候,取出vm._events中存储的方法,调用执行

  3. $emit 是用来触发事件的,他会根据传入的event在vm_events中找到对应的事件(vm._events[event]),并执行invokeWithErrorHandling(cbs[i], vm, args, vm, info)

  4. 最后我们看invokeWithErrorHandling方法可以发现,他是通过handler.apply(context, args)和handler.call(context)的形式执行对应的方法

接着就可以根据 onon、emit的实现方式来写一个eventBus,请继续往下看👇

2、eventBus 实现

eventBus应用场景:跨组件之间的事件通信。

// libs/event.js

// eventBus
// import { ArraySpliceOne } from './utils'

function EventBus() {
  this._events = {}
}

EventBus.prototype.on = function (type, fn, ctx = this) {
  if (!this._events[type]) {
    this._events[type] = []
  }
  this._events[type].push([fn, ctx])
}

EventBus.prototype.once = function (type, fn, ctx = this) {
  function magic() {
    this.off(type, magic)
    fn.apply(ctx, arguments)
  }
  magic.fn = fn
  this.on(type, magic)
}

EventBus.prototype.off = function (type, fn) {
  let _events = this._events[type]
  if (!_events) {
    return
  }
  if (!fn) {
    this._events[type] = null
    return
  }
  let count = _events.length
  while (count--) {
    if (_events[count][0] === fn || (_events[count][0] && _events[count][0].fn === fn)) {
      // ArraySpliceOne(_events, count)
_events.splice(count, 1)
    }
  }
}

EventBus.prototype.emit = function (type) {
  let events = this._events[type]
  if (!events) { return }

  let len = events.length
  let copyEvents = [...events]
  for (let i = 0; i < len; i++) {
    let event = copyEvents[i]
    let [fn, ctx] = event
    if (fn) {
      fn.apply(ctx, [].slice.call(arguments, 1))
    }
  }
}

EventBus.prototype.offAll = function () {
  this._events = {}
}
export default new EventBus()

2.1、使用

  1. libs/index.js

// 挂载到vue原型上,以方便使用。(当然也可以不在页面里面导入使用)

import Vue from 'vue'
import EventBus from './event'

Vue.prototype.$EventBus = EventBus
  1. main.js
// main入口文件引入
import './libs'
  1. 组件内使用

home/a.vue

// home/a.vue

this.$EventBus.emit('optClick', obj)

about/b.vue

// about/b.vue
mounted() {
    this.$EventBus.on('optClick', (data) => {
        this.optClick(data)
    })
}
methods: {
    optClick(){}
},
beforeDestroy() {
    this.$EventBus.off('optClick')
},

最后

实现逻辑比较简单,重要的是编程思想的学习领会。


🎈🎈🎈

🌹 本篇完,关注我,你会发现一个踏实努力的宝藏前端😊,让我们一起学习,共同成长吧。

🎉 喜欢的小伙伴记得点赞关注收藏哟,回看不迷路 😉

✨ 欢迎大家转发、评论交流

🎊 蟹蟹😊

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿