首页>>前端>>Vue->Vue 事件原理(从源码角度带你分析)(4)

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

时间:2023-11-30 本站 点击:0

之前我们已经介绍了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: anyexport 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原理已经大致介绍完毕了,欢迎交流探讨。

原文:https://juejin.cn/post/7097405336251793438


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3773.html