24小时服务热线:
1866-9080-178
您当前的位置:首页>新闻资讯>前端技术
微信公众平台

扫描左侧二维码关注驭云思创官方微信,实时了解公司最新动态!

Vue源码探究-事件系统
云南驭云思创 时间:2019-02-26 14:56:23

紧跟着生命周期之后的就是继续初始化事件相关的属性和方法。整个事件系统的代码相对其他模块来说非常简短,分几个部分来详细看看它的具体实现。

头部引用

import {   tip,   toArray,   hyphenate,   handleError,   formatComponentName } from '../util/index' import { updateListeners } from '../vdom/helpers/index' 复制代码

头部先是引用了的一些工具方法,没有什么难点,具体可以查看相应文件。唯一值得注意的是引用自虚拟节点模块的一个叫 updateListeners 方法。顾名思义,是用来更新监听器的,至于为什么要有这样的一个方法,主要是因为如果该实例的父组件已经存在一些事件监听器,为了正确捕获到事件并向上冒泡,父级事件是需要继承下来的,这个原因在下面的初始化代码中有佐证;另外,如果在实例初始化的时候绑定了同名的事件处理器,也需要为同名事件添加新的处理器,以实现同一事件的多个监听器的绑定。

事件初始化

// 定义并导出initEvents函数,接受Component类型的vm参数 export function initEvents (vm: Component) {   // 创建例的_events属性,初始化为空对象   vm._events = Object.create(null)   // 创建实例的_hasHookEvent属性,初始化为false   vm._hasHookEvent = false   // 初始化父级附属事件   // init parent attached events   const listeners = vm.$options._parentListeners   // 如果父级事件存在,则更新实例事件监听器   if (listeners) {     updateComponentListeners(vm, listeners)   } } // 设置target值,目标是引用实例 let target: any // 添加事件函数,接受事件名称、事件处理器、是否一次性执行三个参数 function add (event, fn, once) {   if (once) {     target.$once(event, fn)   } else {     target.$on(event, fn)   } } // 移除事件函数,接受事件名称和时间处理器两个参数 function remove (event, fn) {   target.$off(event, fn) } // 定义并导出函数updateComponentListeners,接受实例对象,新旧监听器参数 export function updateComponentListeners (   vm: Component,   listeners: Object,   oldListeners: ?Object ) {   // 设置target为vm   target = vm   // 执行更新监听器函数,传入新旧事件监听对象、添加事件与移除事件函数、实例对象   updateListeners(listeners, oldListeners || {}, add, remove, vm)   // 置空引用   target = undefined } 复制代码

如上述代码所示,事件监听系统的初始化首先是创建了私有的事件对象和是否有事件钩子的标志两个属性,然后根据父级是否有事件处理器来决定是否更新当前实例的事件监听器,具体如何实现监听器的更新,贴上这段位于虚拟节点模块的辅助函数中的代码片段来仔细看看。

更新事件监听器

// 定义并导出updateListeners哈数 // 接受新旧事件监听器对象,事件添加和移除函数以及实例对象参数。 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赋值为新的事件对象     def = cur = on[name]     // 为old赋值为旧的事件对象     old = oldOn[name]     // 标准化事件对象并赋值给event。     // normalizeEvent函数主要用于将传入的带有特殊前缀的事件修饰符分解为具有特定值的事件对象     event = normalizeEvent(name)     // 下面代码是weex框架专用,处理cur变量和格式化好的事件对象的参数属性     /* 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)) {       // 如果新事件对象cur的fns属性不存在       if (isUndef(cur.fns)) {         // 创建函数调用器并重新复制给cur和on[name]         cur = on[name] = createFnInvoker(cur)       }       // 添加新的事件处理器       add(event.name, cur, event.once, event.capture, event.passive, event.params)     // 如果新旧事件不完全相等     } else if (cur !== old) {       // 用新事件处理函数覆盖旧事件对象的fns属性       old.fns = cur       // 将事件对象重新复制给on       on[name] = old     }   }   // 遍历旧事件监听器   for (name in oldOn) {     // 如果新事件对象不存在     if (isUndef(on[name])) {       // 标准化事件对象       event = normalizeEvent(name)       // 移除事件处理器       remove(event.name, oldOn[name], event.capture)     }   } } 复制代码

这段代码中用到了 normalizeEvent 和 createFnInvoker 两个主要的函数来完成更新监听器的实现,代码与 updateListeners 函数位于同一文件中。

  • normalizeEvent:主要是用于返回一个定制化的事件对象,这个函数接受4个必选参数和2两个可选参数,分别是事件名称name属性、是否一次性执行的once属性、是否捕获事件的capture属性、是否使用被动模式passive属性、事件处理器handler方法、事件处理器参数params数组。属性的含义都比较好理解,特别注意一下 once、capture、passive 属性,这三个属性是用来修饰事件的,分别对应了 ~、!、& 修饰符,贴上一个官方文档中的使用示例,引用自事件 & 按键修饰符。启动被动模式的用途是使事件处理器无法阻止默认事件,比如 <a> 标签自带的链接跳转事件,如果设置passive为true,则事件处理器即便是设置了阻止默认事件也是没办法阻止跳转的。

on: {   '!click': this.doThisInCapturingMode,   '~keyup': this.doThisOnce,   '~!mouseover': this.doThisOnceInCapturingMode } 复制代码
  • createFnInvoker: 接受一个fns参数,可以传入一个事件处理器函数,也可以传入一个包含多个处理器的数组。在该函数内部定义了一个 invoker 函数并且最终返回它,函数有一个fns属性是用来存放所传入的处理器的,调用这个函数后,会按fns的类型来分别执行处理器数组的调用或单个处理器的调用。这个实现即是真正执行事件处理器调用的过程。

事件相关的原型方法

在事件的初始化过程里有用到几个以 & 开头的类原型方法,它们是在mixin函数里挂载到核心类上的。初始化的时候定义的方法都是在这些方法的基础上再进行了一次封装,其绑定事件、触发事件和移除事件的具体实现都在这些方法中,当然不会放过对这些细节的探索。

// 导出eventsMixin函数,接收形参Vue, // 使用Flow进行静态类型检查指定为Component类 export function eventsMixin (Vue: Class<Component>) {   // 定义hook正则检验   const hookRE = /^hook:/   // 给Vue原型对象挂载$on方法   // 参数event可为字符串或数组类型,fn是事件监听函数   // 方法返回实例对象本身   Vue.prototype.$on = function (event: string | Array<string>, fn: Function): Component {     // 定义实例变量     const vm: Component = this     // 如果传入的event参数是数组,遍历event数组,为所有事件注册fn监听函数     if (Array.isArray(event)) {       for (let i = 0, l = event.length; i < l; i++) {         this.$on(event[i], fn)       }     } else {       // event参数为字符串时,检查event事件监听函数数组是否存在       // 已存在事件监听数组则直接添加新监听函数       // 否则建立空的event事件监听函数数组,再添加新监听函数   &