Vue 事件原理(从源码角度带你分析)(4)

978 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

之前我们已经介绍了event的编译过程(点击这里跳转),接下来我们分析在Vue初始化和更新的过程中event的内部是如何生成的。

event生成之自定义事件

Vueevent事件分为原生DOM事件与自定义事件,原生DOM事件的处理(点击这里跳转),我们上一节已经分析过了。这一节我们来分析下自定义事件。

自定义事件是用在组件节点上的,组件节点上定义的事件可以分为两类:一类是原生DOM事件( 在vue2.x版本在组件节点上使用原生DOM事件需要添加native修饰符),另一类就是自定义事件。

下面我们来分析自定义事件的流程:

创建组件vnode

创建组件vnode(虚拟节点)的时候会执行createComponent函数,其中有如下逻辑:

export function createComponent (
  Ctor: Class<Component> | Function | Object | void,
  data: ?VNodeData,
  context: Component,
  children: ?Array<VNode>,
  tag?: string
): VNode | Array<VNode> | void {

  ......
  // extract listeners, since these needs to be treated as
  // child component listeners instead of DOM listeners
  // 自定义事件赋值给listeners
  const listeners = data.on
  // replace with listeners with .native modifier
  // so it gets processed during parent component patch.
  // native事件赋值给data.on,这样原生方法直接就上一节相同的逻辑了
  data.on = data.nativeOn

  ......
  // return a placeholder vnode
  // 创建占位符vnode
  const name = Ctor.options.name || tag
  // 生成虚拟节点的时候,将listeners当参数传入
  const vnode = new VNode(
    `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    data, undefined, undefined, undefined, context,
    { Ctor, propsData, listeners, tag, children },
    asyncFactory
  )
  // 返回vnode
  return vnode
}

创建组件vnode的过程中会将组件节点上的定义的自定义事件赋值给listeners变量,同时将组件节点上定义的原生事件赋值给data.on属性,这样,组件的原生事件就会执行如同上一节生成原生事件相同的逻辑。然后在创建组件vnode的时候,会将listeners(缓存了自定义事件)当做第七个参数(componentOptions)的属性值。

vnode创建完成之后,在初始化组件的时候,会执行initInternalComponent函数:

组件初始化

initInternalComponent


export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
  // 子组件构造器的options(配置项)
  const opts = vm.$options = Object.create(vm.constructor.options)
  // ....
  // 我们之前创建的节点的第七个参数(componentOptions)
  const vnodeComponentOptions = parentVnode.componentOptions
 
  // 子组件构造器的_parentListeners属性指向之前定义的listeners(组件自定义事件)
  opts._parentListeners = vnodeComponentOptions.listeners
  // ...
}

执行完这些配置项的生成之后,会初始化子组件事件


export function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  // 有listeners,执行updateComponentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

listeners非空,执行updateComponentListeners函数:


let target: any
export function updateComponentListeners (
  vm: Component,
  listeners: Object,
  oldListeners: ?Object
) {
  // target指向当前实例
  target = vm
  // 执行updateListeners
  updateListeners(listeners, oldListeners || {}, add, remove, vm)
  target = undefined
}

这个地方同样执行updateListeners函数,与上一节原生DOM事件的生成相同,但与原生DOM事件的生成有几处不同之处,如下addremove函数的定义。

function add (event, fn, once) {
  if (once) {
    // 如果有once属性,执行$once方法
    target.$once(event, fn)
  } else {
    否则执行$on方法
    target.$on(event, fn)
  }
}

function remove (event, fn) {
  // remove方法是执行$off方法
  target.$off(event, fn)
}

关于$once$on$off函数都定义在eventsMixin中:

export function eventsMixin (Vue: Class<Component>) {
  const hookRE = /^hook:/
  Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  ......
 }

  Vue.prototype.$once = function (event: string, fn: Function): Component {
    ......
  }

  Vue.prototype.$off = function (event?: string | Array<string>, fn?: Function): Component {
    ......
  }

  Vue.prototype.$emit = function (event: string): Component {
    ......
  }
}

$on

Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {
  // 当前实例就是调用该方法的实例
  const vm: Component = this
  // 如果event是数组,遍历数组,依次执行$on函数
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      this.$on(event[i], fn)
    }
  } else {
    // 将当前实例的_events属性初始化为空数组并push当前添加的函数
    (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
}

$on的逻辑就是将当前的方法存入当前实例vm._events属性中。

$once

Vue.prototype.$once = function (event: string, fn: Function): Component {
  // 当前实例就是调用该方法的实例
  const vm: Component = this
  // 定义on函数
  function on () {
    // 执行$off销毁当前事件
    vm.$off(event, on)
    // 执行函数fn
    fn.apply(vm, arguments)
  }
  // on的fn属性指向当前传入的函数
  on.fn = fn
  // 将on函数存入vm._events中
  vm.$on(event, on)
  return vm
}

$once的逻辑就是对传入的fn函数做了一层封装,生成了一个内部函数onon.fn属性指向传入函数fn,将on函数存入实例的_events属性对象中,这样执行完一次这个函数后,该函数就被销毁了。

$off

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如果是数组,遍历该数组,依次调用$off函数
    if (Array.isArray(event)) {
      for (let i = 0, l = event.length; i < l; i++) {
        this.$off(event[i], fn)
      }
      // 返回
      return vm
    }
    // specific event
    // 唯一的event
    const cbs = vm._events[event]
    // cbs未定义,直接返回
    if (!cbs) {
      return vm
    }
    // fn未定义(未传入fn的情况下),vm._events[event]赋值为空,直接返回
    if (!fn) {
      vm._events[event] = null
      return vm
    }
    // fn定义了
    if (fn) {
      // specific handler
      let cb
      let i = cbs.length
      // 遍历cbs对象
      while (i--) {
        cb = cbs[i]
        // 如果查找到有属性与fn相同
        if (cb === fn || cb.fn === fn) {
          // 移除该属性,跳出循环
          cbs.splice(i, 1)
          break
        }
      }
    }
    return vm
  }

$off的作用就是移除vm._events对象上定义的事件函数。

eventsMixin中还定义了一个函数$emit,在组件通讯的时候经常使用:

$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}".`
        )
      }
    }
    // 拿到vm._events的event事件上的所有函数
    let cbs = vm._events[event]
    // 存在cbs
    if (cbs) {
      // cbs转化
      cbs = cbs.length > 1 ? toArray(cbs) : cbs
      // 其他参数转化成数组
      const args = toArray(arguments, 1)
      // 遍历cbs,依次执行其中的函数
      for (let i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(vm, args)
        } catch (e) {
          handleError(e, vm, `event handler for "${event}"`)
        }
      }
    }
    return vm
  }

从源码上可以看出,在我们平时开发过程中,其实看似通过$emit方法调用父组件上的函数,本质上是调用组件自身实例上定义的函数,而这个函数是在组件生成的过程中传入到子组件的配置项中的。

还有一点值得提一下,组件自定义事件的事件调用,其实就是非常经典的事件中心的实现。而我们在Vue开发过程中常用的eventBus的实现,原理也是同上。

到此为止,关于Vueevent原理已经大致介绍完毕了,欢迎交流探讨。