vue-source-code-analysic(3)

阅读Vue源码有感(3)

Posted by Mickey on May 3, 2018

前一篇博客从源码的角度讲述了vue作为一个MVVM框架,data是如何和视图进行绑定的,这篇博客将讲述vue2中的4个事件API

vue2和vue1有很多的不同,总的来说,框架变得更加严格,鲁棒性较好,其中非常重要的一个改变就是移除了$broadcast和$dispatch两个方法,优化了性能

vue2中提供了4个事件API挂在vm上,分别是$emit, $on, $off, $once

  • $emit: 触发一个自定义事件,本组件和父亲组件能够监听
  • $on: 用于在vm实例上监听一个自定义事件,该事件可以被$emit触发
  • $off: 移除vm实例上监听的事件或者事件的回调函数
  • $once: 用于在vm实例上监听一次自定义事件,事件触发之后,移除该事件

vue事件处理源码地址

初始化事件

vm实例初始化的时候,在vm上绑定了一个_events对象,用于存放自定义对象,对象的key值为自定义事件名,value为一个数组,存放了该事件所有的回调函数

vm._events = {
  eventName: [cb1, cb2, cb3],
};

/**
 * 初始化事件
 */
export function initEvents (vm: Component) {
  /**
   * 在vm上创建一个_events对象,用来存放事件。
   */
  vm._events = Object.create(null)
  /**
   * 这个bool标志位来表明是否存在钩子,而不需要通过哈希表的方法来查找是否有钩子
   * 这样做可以减少不必要的开销,优化性能
   */
  vm._hasHookEvent = false
  // init parent attached events
  /**
   * 初始化父组件attach的事件
   */
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

let target: any

/**
 * once为true的时候,执行$once注册事件,否则调用$on注册事件
 */
function add (event, fn, once) {
  if (once) {
    target.$once(event, fn)
  } else {
    target.$on(event, fn)
  }
}

/**
 * 销毁事件
 */
function remove (event, fn) {
  target.$off(event, fn)
}

/**
 * 更新组件的监听事件
 */
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  target = vm
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  target = undefined
}

/**
 * 更新监听事件(在这里就是将父组件的事件capture函数放入vm实例的callback数组里)
 */
export function updateListeners (
  on: Object,
  oldOn: Object,
  add: Function,
  remove: Function,
  vm: Component
) {
  let name, def, cur, old, event
  for (name in on) {
    def = cur = on[name]
    old = oldOn[name]
    event = normalizeEvent(name)
    /* istanbul ignore if */
    if (__WEEX__ && isPlainObject(def)) {
      cur = def.handler
      event.params = def.params
    }
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        `Invalid handler for event "${event.name}": got ` + String(cur),
        vm
      )
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur)
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params)
    } else if (cur !== old) {
      old.fns = cur
      on[name] = old
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name)
      remove(event.name, oldOn[name], event.capture)
    }
  }
}

$on方法

$on方法用来在vm实例上监听一个自定义事件,该事件可用$emit触发

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  const vm: Component = this
  /**
   * 如果传入的事件不是string而是一个数组的话,则调用自身,为数组的每一个成员绑上fn的cb函数 
   */
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$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
    /**
     * 这里在注册事件的时候标记bool值也就是个标志位来表明存在钩子,而不需要通过哈希表的方法
     * 来查找是否有钩子,这样做可以减少不必要的开销,优化性能
     */
    const hookRE = /^hook:/ // vue的生命周期的钩子事件都是hook:开头的
    if (hookRE.test(event)) {
      vm._hasHookEvent = true
    }
  }
  return vm
}

$once方法

$once监听一个只能触发一次的事件,在触发以后会自动移除该事件

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
}

$off方法

$off方法用于移除自定义事件

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
  /*如果event是数组则递归注销事件*/
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$off(event[i], fn)
    }
    return vm
  }
  // specific event
  const cbs = vm._events[event]
  /*Github:https://github.com/answershuto*/
  /*本身不存在该事件则直接返回*/
  if (!cbs) {
    return vm
  }
  /*如果只传了event参数则注销该event方法下的所有方法*/
  if (arguments.length === 1) {
    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
}

$emit方法

$emit用来触发指定的自定义事件

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
}

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)
    /*遍历执行*/
    for (let i = 0, l = cbs.length; i < l; i++) {
      cbs[i].apply(vm, args)
    }
  }
  return vm
}