Vue 事件相关实例方法---on/emit/off/once

Posted linjunfu

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Vue 事件相关实例方法---on/emit/off/once相关的知识,希望对你有一定的参考价值。

一、初始位置

平常项目中写逻辑,避免不了注册/触发各种事件

今天来研究下 Vue 中,我们平常用到的关于 on/emit/off/once 的实现原理

关于事件的方法,是在 Vue 项目下面文件中的 eventsMixin 注册的

src/core/instance/index.js

import  initMixin  from './init'
import  stateMixin  from './state'
import  renderMixin  from './render'
import  eventsMixin  from './events'
import  lifecycleMixin  from './lifecycle'
import  warn  from '../util/index'

function Vue(options) 
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) 
    warn('Vue is a constructor and should be called with the `new` keyword')
  
  this._init(options)


initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue) // 此处初始化 Vue 关于事件相关的实例方法
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue


二、源码解析

进入到 src/core/instance/events.js 文件中

这边提取了 on/emit/off/once 的相关代码,并做了注释

src/core/instance/events.js

/**
 * @describtion 注册事件以及触发事件时要执行的函数
 * @param event  string | Array<string> 要注册的事件名,可以是个字符串,也可以是个数组,数组元素也是字符串
 * @param fn     Function 要注册的事件函数
 * @return Component  返回 Vue 实例
 */
Vue.prototype.$on = function(event: string | Array<string>, fn: Function): Component 
  const vm: Component = this
  // 先判断传进来的 event 是否是个数组
  if (Array.isArray(event)) 
    // 是数组,则循环进行事件注册
    // 多个事件名可以绑定同个函数
    for (let i = 0, l = event.length; i < l; i++) 
      vm.$on(event[i], fn)
    
   else 
    // event 不是数组
    // event 是个字符串
    // 先判断 vm._events[event] 是否存在, 不存在则设置为空数组 []
    // 这个 vm._events 在 new Vue 时候, Vue 里面 执行 this._init() 中
    // 执行了 initEvents(vm)
    // 在 initEvents(vm) 中
    // export function initEvents(vm: Component) 
    //   vm._events = Object.create(null) // 这里创建了 _events 这个对象,用来存储事件
    //   vm._hasHookEvent = false
    //   // init parent attached events
    //   const listeners = vm.$options._parentListeners
    //   if (listeners) 
    //     updateComponentListeners(vm, listeners)
    //   
    // 
    ;(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
    // 在注册的时候使用标记过的布尔值代替哈希查找,消费hook事件
    if (hookRE.test(event)) 
      vm._hasHookEvent = true
    
  
  return vm



/**
 * @describtion 和 $on 一样,注册事件以及触发事件时要执行的函数,但是只执行一次就销毁
 * @param event  string 要注册的事件名,是个字符串
 * @param fn     Function 要注册的事件函数
 * @return Component  返回 Vue 实例
 */
Vue.prototype.$once = function(event: string, fn: Function): Component 
  const vm: Component = this
  // 将目标函数 fn 包装起来
  // 注册时候使用包装的 on 函数注册
  // 这样 on 函数被执行一次时,首先把自己从注册事件列表中销毁
  // 然后执行实际的目标函数 fn

  // 如果是一开始就使用目标函数 fn 注册
  // 然后在目标函数 fn 执行时候,销毁fn
  // 做不到销毁自己的同时还能执行自己,所以需要把fn进行一次包装
  function on() 
    vm.$off(event, on)
    fn.apply(vm, arguments)
  
  // 因为对目标函数做了包装,此处是方便销毁事件时候做判断是否有事件要销毁以及要销毁的是哪个 fn
  on.fn = fn 
  vm.$on(event, on)
  return vm



/**
 * @describtion 销毁事件以及触发事件时要执行的函数
 * @param event?  string | Array<string> 可选。要销毁的事件名,可以是个字符串,也可以是个数组,数组元素也是字符串
 * @param fn?     Function 要销毁的事件函数 可选。
 * @return Component  返回 Vue 实例
 */
Vue.prototype.$off = function(event?: string | Array<string>, fn?: Function): Component 
  const vm: Component = this
  // all
  // 如果没有参数,则将 vm_events 设置为空,表示销毁全部事件
  if (!arguments.length) 
    vm._events = Object.create(null)
    return vm
  
  // array of events
  // 如果 event 是个数组,则遍历 event,对每个事件进行销毁
  if (Array.isArray(event)) 
    for (let i = 0, l = event.length; i < l; i++) 
      vm.$off(event[i], fn)
    
    return vm
  
  // specific event
  // 上面两个是特殊情况,这里才是正常销毁逻辑
  // 先通过传入的 event 字符串从 _events 对象中去取值
  // 判断该 事件名底下是否有绑定的目标函数,没有则返回当前组件实例,啥也不做
  const cbs = vm._events[event]
  if (!cbs) 
    return vm
  
  // 或者没有传入之前注册时候的目标函数
  // 那么就将 event 对应的所有目标函数都销毁
  // vm._events[event] = null
  if (!fn) 
    vm._events[event] = null
    return vm
  

  // specific handler
  // 如果有传入 目标函数
  // 对取出的 event 对应的 目标函数进行倒序遍历
  // vm._events[event] 的值,经过前面的过滤,到这里一定是个数组
  // 倒序遍历一个个数组元素,判断每一个元素与传入要销毁的目标函数是否相等
  // 相等,则使用 splice 进行删除
  // 删除数组的操作使用倒序处理,不至于在删除元素的时候,后续的元素序号向前进位,导致处理结果有误
  let cb
  let i = cbs.length
  while (i--) 
    cb = cbs[i]
    if (cb === fn || cb.fn === fn) 
      cbs.splice(i, 1)
      break
    
  
  return vm



/**
 * @describtion 触发事件
 * @param event  string 要触发的事件名,是个字符串
 * @return Component  返回 Vue 实例
 */
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".`)
    
  
  //   通过传入的 event 从 _events 对象中获取目标函数
  let cbs = vm._events[event]
  if (cbs) 
    // 如果有相应的目标函数
    // Convert an Array-like object to a real Array.
    // toArray 将一个类数组转换成真正的数组
    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++) 
      // 该函数调用了当前目标函数,并处理目标函数的异常
      // 比如 目标函数 返回一个 Promise 这里添加了 catch 处理
      invokeWithErrorHandling(cbs[i], vm, args, vm, info)
    
  
  return vm


三、实现例子

项目地址放在github上了,有需要的可以看下
Vue 的事件方法类实现例子

模式实现一个 Vue 的事件方法类

class VueEvent 
  constructor() 
    this._events = Object.create(null)
  
  $on(event, fn) 
    const vm = this
    // 先判断传进来的 event 是否是个数组
    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] || (vm._events[event] = [])).push(fn)
    
    return vm
  
  $once(event, fn) 
    const vm = this
    // 将目标函数 fn 包装起来
    // 注册时候使用包装的 on 函数注册
    // 这样 on 函数被执行一次时,首先把自己从注册事件列表中销毁
    // 然后执行实际的目标函数 fn

    // 如果是一开始就使用目标函数 fn 注册
    // 然后在目标函数 fn 执行时候,销毁fn
    // 做不到销毁自己的同时还能执行自己,所以需要把fn进行一次包装
    function on() 
      vm.$off(event, on)
      fn.apply(vm, arguments)
    
    // 因为对目标函数做了包装,此处是方便销毁事件时候做判断,是否有事件要销毁以及要销毁的是哪个 fn
    on.fn = fn 
    vm.$on(event, on)
    return vm
  
  $off(event, fn) 
    const vm = this

    // 如果没有参数,则将 vm_events 设置为空,表示销毁全部事件
    if (!arguments.length) 
      vm._events = Object.create(null)
      return vm
    

    // 如果 event 是个数组,则遍历 event,对每个事件进行销毁
    if (Array.isArray(event)) 
      for (let i = 0, l = event.length; i < l; i++) 
        vm.$off(event[i], fn)
      
      return vm
    

    // 上面两个是特殊情况,这里才是正常销毁逻辑
    // 先通过传入的 event 字符串从 _events 对象中去取值
    // 判断该 事件名底下是否有绑定的目标函数,没有则返回当前组件实例,啥也不做
    const cbs = vm._events[event]
    if (!cbs) 
      return vm
    

    // 或者没有传入之前注册时候的目标函数
    // 那么就将 event 对应的所有目标函数都销毁
    // vm._events[event] = null
    if (!fn) 
      vm._events[event] = null
      return vm
    

    // 如果有传入 目标函数
    // 对取出的 event 对应的目标函数进行倒序遍历
    // vm._events[event] 的值,经过前面的过滤,到这里一定是个数组
    // 倒序遍历一个个数组元素,判断每一个元素与传入要销毁的目标函数是否相等
    // 相等,则使用 splice 进行删除
    // 删除数组的操作使用倒序处理,不至于在删除元素的时候,后续的元素序号向前进位,导致处理结果有误
    let cb
    let i = cbs.length
    while (i--) 
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) 
        cbs.splice(i, 1)
        break
      
    
    return vm
  
  $emit(event) 
    const vm = this
    // 通过传入的 event 从 _events 对象中获取目标函数
    let cbs = vm._events[event]
    if (cbs) 
      // 如果有相应的目标函数
      // 获取除了第一个事件名之外的其他参数
      const args = Array.prototype.slice.call(arguments, 1)
      // 对得到的目标函数进行遍历,并传入相关参数
      for (let i = 0, l = cbs.length; i < l; i++) 
        // 这里就不做 promise 的处理了,直接调用
        cbs[i].apply(vm, args)
      
    
    return vm
  


四、使用

let ev = new VueEvent()

// test $on
ev.$on('onEv', function(emitParam) 
  console.log('test $on: ', emitParam)
  console.log('onEv on')
  console.log('\n************\n')
)
setTimeout(() => 
  ev.$emit('onEv', 'emit 1')
, 0)
setTimeout(() => 
  ev.$emit('onEv', 'emit 2')
, 1000)
// 输出
// test $on:  emit 1
// onEv on

// ************

// test $on:  emit 2
// onEv on


// test $once
ev.$once('onceEv', function(emitParam) 
  console.log('test $once: ', emitParam)
  console.log('onceEv on')
  console.log('\n************\n')
)
setTimeout(() => 
  ev.$emit('onceEv', 'emit 3')
, 2000)
setTimeout(() => 
  ev.$emit('onceEv', 'emit 4')
, 3000)
// 输出
// test $once:  emit 3
// onceEv on


// test $off
ev.$on('offEv', function(emitParam) 
  console.log('test $off: ', emitParam)
  console.log('offEv on')
  console.log('\n************\n')
)
setTimeout(() => 
  ev.$emit('offEv', 'emit 5')
, 4000)
setTimeout(() => 
  ev.$emit('offEv', 'emit 6')
  ev.$off('offEv')
, 5000)
setTimeout(() => 
  ev.$emit('offEv', 'emit 7')
, 6000)
// 输出
// test $off:  emit 5
// offEv on

// ************

// test $off:  emit 6
// offEv on

以上是关于Vue 事件相关实例方法---on/emit/off/once的主要内容,如果未能解决你的问题,请参考以下文章

vue 实例事件

vue实例中的事件和方法

vue - 生命周期

vue实例的生命周期

vue父组件点击触发子组件事件的实例讲解

Vue实例方法之事件的实现