$emit| $on | $off |$once的实现方式

521 阅读2分钟

使用方式

  • $emit$on的使用方法

    • vm.$on('test', function (msg) {
        console.log(msg)
      })
      vm.$emit('test', 'hi')
      // => "hi"
      
  • $offonce的使用方法

    • this.$off('event_name')
      
      • 移除自定义事件监听器。
        • 如果没有提供参数,则移除所有的事件监听器;
        • 如果只提供了事件,则移除该事件所有的监听器;
        • 如果同时提供了事件与回调,则只移除这个回调的监听器 ,删除事件名称对应事件数组中执行的函数方法。
    • this.$once('event_name',function(){})
      
      • 监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

实现方式

  • $on

    • 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)
           if (hookRE.test(event)) {
             vm._hasHookEvent = true
           }
         }
         return vm
       }
      
      • 方法传入的event参数可以是数组或是字符串
      • 如果是字符串就将该方法放置到_events[event]数组中,判断一下是否是hook:事件,如果是需要在对象上添加属性
      • 如果是数组的话就将数组里面的数据循环存放到this._event[event]
  • $emit

    • 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) // 只获取索引从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
        }
      
      • 开发环境下:event事件名称不是全是小写,并且this._events[event]中之前已经注册过了全是小写的方法就将触发提示
      • 循环执行this._events[event]中的方法
    • // 讲一个数组对象转换成真正的数组
      export function toArray (list: any, start?: number): Array<any> {
        start = start || 0
        let i = list.length - start
        const ret: Array<any> = new Array(i)
        while (i--) {
          ret[i] = list[i + start]
        }
        return ret
      }
      
      • 可以获取指定位置开始的数组数据
    • 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)`))
            res._handled = true
          }
        } catch (e) {
          handleError(e, vm, info)
        }
        return res
      }
      
      • 执行事件函数,并提示相关的错误信息

this.$emitthis.$on是如何实现在子组件中触发了$emit之后$on监听到数据了呢?

回答: 当编译之后this.$on所绑定的事件和方法都被放置到this._events数组中,当在子组件中触发了this.$emit之后将获取this._events中的对应的事件进行执行。

  • $off

    • Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
          const vm: Component = 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
        }
      
      • 主要是就是根据event事件名称来在this._events中删除指定的事件方法
  • $once

    •  Vue.prototype.$once = function (event: string, fn: Function): Component {
          const vm: Component = this
          function on () { // once主要是删除在this._events中的事件方法 然后执行事件
            vm.$off(event, on)
            fn.apply(vm, arguments)
          }
          on.fn = fn
          vm.$on(event, on) // 先在this._events中绑定事件
          return vm
        }
      
      • 先利用this.$on方法在this._events[event]中绑定事件,然后将在this.$emit执行的时候,调用on方法。在on回调中利用this.off来删除this._events中指定的方法,然后再对方法进行执行从而实现只执行了一次。