Vue源码探究-事件系统

本篇代码位于vue/src/core/instance/events.js

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

头部引用


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)
}
}
}

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

  • normalizeEvent:主要是用于返回一个定制化的事件对象,这个函数接受4个必选参数和2两个可选参数,分别是事件名称name属性、是否一次性执行的once属性、是否捕获事件的capture属性、是否使用被动模式passive属性、事件处理器handler方法、事件处理器参数params数组。属性的含义都比较好理解,特别注意一下 oncecapturepassive 属性,这三个属性是用来修饰事件的,分别对应了 ~!& 修饰符,贴上一个官方文档中的使用示例,引用自事件 & 按键修饰符。启动被动模式的用途是使事件处理器无法阻止默认事件,比如 <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&lt;Component&gt;) {
// 定义hook正则检验
const hookRE = /^hook:/
// 给Vue原型对象挂载$on方法
// 参数event可为字符串或数组类型,fn是事件监听函数
// 方法返回实例对象本身
Vue.prototype.$on = function (event: string | Array&lt;string&gt;, fn: Function): Component {
// 定义实例变量
const vm: Component = this
// 如果传入的event参数是数组,遍历event数组,为所有事件注册fn监听函数
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i &lt; l; i++) {
this.$on(event[i], fn)
}
} else {
// event参数为字符串时,检查event事件监听函数数组是否存在
// 已存在事件监听数组则直接添加新监听函数
// 否则建立空的event事件监听函数数组,再添加新监听函数
(vm._events[event] || (vm._events[event] = [])).push(fn)
// 此处做了性能优化,使用正则检验hook:是否存在的布尔值
// 而不是hash值查找设置实例对象的_hasHookEvent值
// 此次优化是很久之前版本的修改,暂时不太清楚以前hash值查找是什么逻辑,留待以后查证
// 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
}
// 为Vue原型对象挂载$once方法
// 参数event只接受字符串,fn是监听函数
Vue.prototype.$once = function (event: string, fn: Function): Component {
// 定义实例变量
const vm: Component = this
// 创建on函数
function on () {
// 函数执行后先清除event事件绑定的on监听函数,即函数本身
// 这样以后就不会再继续监听event事件
vm.$off(event, on)
// 在实例上运行fn监听函数
fn.apply(vm, arguments)
}
// 为on函数设置fn属性,保证在on函数内能够正确找到fn函数
on.fn = fn
// 为event事件注册on函数
vm.$on(event, on)
// 返回实例本身
return vm
}
// 为Vue原型对象挂载$off方法
// event参数可为字符串或数组类型
// fn是监听函数,为可选参数
Vue.prototype.$off = function (event?: string | Array&lt;string&gt;, fn?: Function): Component {
// 定义实例变量
const vm: Component = this
// 如果没有传入参数,则清除实例对象的所有事件
// 将实例对象的_events私有属性设置为null,并返回实例
// all
if (!arguments.length) {
vm._events = Object.create(null)
return vm
}
// 如果event参数传入数组,清除所有event事件的fn监听函数返回实例
// 这里是$off方法递归执行,最终会以单一事件为基础来实现监听的清除
// array of events
if (Array.isArray(event)) {
for (let i = 0, l = event.length; i &lt; l; i++) {
this.$off(event[i], fn)
}
return vm
}
// 如果指定单一事件,将事件的监听函数数组赋值给cbs变量
// specific event
const cbs = vm._events[event]
// 如果没有注册此事件监听则返回实例
if (!cbs) {
return vm
}
// 如果没有指定监听函数,则清除所有该事件的监听函数,返回实例
if (!fn) {
vm._events[event] = null
return vm
}
// 如果指定监听函数,则遍历事件监听函数数组,移除指定监听函数返回实例
if (fn) {
// 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
}
// 为Vue原型对象挂载$emit方法,只接受单一event
Vue.prototype.$emit = function (event: string): Component {
// 定义实例变量
const vm: Component = this
// 在非生产环境下,传入的事件字符串如果是驼峰值且有相应的小写监听事件
// 则提示事件已注册,且无法使用驼峰式注册事件
if (process.env.NODE_ENV !== 'production') {
const lowerCaseEvent = event.toLowerCase()
if (lowerCaseEvent !== event &amp;&amp; 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}".`
)
}
}
// 将事件监听函数数组赋值 给cbs
let cbs = vm._events[event]
// 如果监听函数数组存在
if (cbs) {
// 重置cbs变量,为何要使用toArray方法转换一次数组不太明白?
cbs = cbs.length &gt; 1 ? toArray(cbs) : cbs
// 将event之后传入的所有参数定义为args数组
const args = toArray(arguments, 1)
// 遍历所有监听函数,为实例执行每一个监听函数,并传入args参数数组
for (let i = 0, l = cbs.length; i &lt; l; i++) {
try {
cbs[i].apply(vm, args)
} catch (e) {
handleError(e, vm, `event handler for "${event}"`)
}
}
}
return vm
}
}

eventsMixin的内容非常直观,分别为实例原型对象挂载了$on$once$off$emit四个方法。这是实例事件监听函数的注册、一次性注册、移除和触发的内部实现。在使用的过程中会对这些实现有一个更清晰的理解。


终于对Vue的事件系统的实现有了一个大致了解,没有什么特别高深的处理,但完整的事件系统的实现有很多细致的功能这里其实并没有特别详细地探讨,比如事件修饰符,可以参考官方文档里的解说会有一个更清晰的了解。事件系统的重要作用首先是为实例制定了一套处理事件的方案和标准,其次是在实例数据更新的过程中保持对事件监听器的更新,这两个部分的处理是最需要细致去琢磨的。

原文地址:https://segmentfault.com/a/1190000016757343

Vue源码探究-事件系统的更多相关文章

  1. Vue源码探究-全局API

    Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...

  2. Vue源码探究-状态初始化

    Vue源码探究-状态初始化 Vue源码探究-源码文件组织 Vue源码探究-虚拟DOM的渲染 本篇代码位于vue/src/core/instance/state.js 继续随着核心类的初始化展开探索其他 ...

  3. Vue源码探究-源码文件组织

    Vue源码探究-源码文件组织 源码探究基于最新开发分支,当前发布版本为v2.5.17-beta.0 Vue 2.0版本的大整改不仅在于使用功能上的优化和调整,整个代码库也发生了天翻地覆的重组.可见随着 ...

  4. Vue源码探究-数据绑定的实现

    Vue源码探究-数据绑定的实现 本篇代码位于vue/src/core/observer/ 在总结完数据绑定实现的逻辑架构一篇后,已经对Vue的数据观察系统的角色和各自的功能有了比较透彻的了解,这一篇继 ...

  5. Vue源码探究-虚拟DOM的渲染

    Vue源码探究-虚拟DOM的渲染 在虚拟节点的实现一篇中,除了知道了 VNode 类的实现之外,还简要地整理了一下DOM渲染的路径.在这一篇中,主要来分析一下两条路径的具体实现代码. 按照创建 Vue ...

  6. vue 源码详解(二): 组件生命周期初始化、事件系统初始化

    vue 源码详解(二): 组件生命周期初始化.事件系统初始化 上一篇文章 生成 Vue 实例前的准备工作 讲解了实例化前的准备工作, 接下来我们继续看, 我们调用 new Vue() 的时候, 其内部 ...

  7. VUE 源码学习01 源码入口

    VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...

  8. 大白话Vue源码系列(03):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

  9. 大白话Vue源码系列(04):生成render函数

    阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...

随机推荐

  1. Python-15-收集参数

    允许用户提供任意数量的参数: def print_params(*params): print(params)   >>> print_params('Testing') ('Tes ...

  2. 爬虫(BeautifulSoup4)——安装

    环境:python3 win10 安装这个心好累啊!网上找了很多办法都安装不成功,后来换了几个安装包,最后4.4.1版本的包终于能用了! https://blog.csdn.net/www520507 ...

  3. VS2012,更新补丁后的残忍--创建项目未找到与约束匹配的导出

    解决方法网址:http://blog.csdn.net/jly4758/article/details/18660945

  4. JAVA基础之Date类、DateFormat类及Calendar类

    个人理解: 关于Date类,进行截取或者转换时一定要注意好数据类型,long类型后面要加上L.时间的原点是1970年.用DateFormat则完成日期与文本之间的转换,特别注意的是:月是用M,时是用H ...

  5. 13.JAVA-包package、import使用

    1.包的定义 之前我们学习java时,生成的class文件都是位于当前目录中,假如出现了同名文件,则会出现文件覆盖问题,因此就需要设置不同的目录(定义包),来解决同名文件冲突问题. 并且在大型项目中, ...

  6. SQL Server数据库log shipping 灾备(Part2 )

    3.配置步骤: 主服务器(A机)设置 (1) 启用Log Shipping Configuration 右键单击需要配置日志传输的数据库->Tasks-> Ship Transaction ...

  7. ZOJ 3469 Food Delivery (区间DP,经典)

    题意: 在x轴上有一家外卖餐馆,有n个顾客站在x轴上不同坐标上且叫了外卖,每个人的脾气不同,每1分钟没有收到外卖就会增加Fi点愤怒值,而外卖小哥的车是有速度的v-1/分钟,问怎样的送餐次序会让所有顾客 ...

  8. 【TensorFlow入门完全指南】基本操作

    众所周知我暂时弃掉了那个音乐生成的坑,原因是我的代码写得还不够纯熟…… 现在我找到了一个项目,用来从代码基础开始补起,同时写下学习笔记. 项目地址:https://github.com/aymeric ...

  9. ABAP system landscape和vue项目webpack构建的最佳实践

    基于Netweaver的ABAP transport route一般都有dev,test和prod三种类型的系统. 而Vue前端项目的webpack build设置也类似. 以SAP成都研究院数字创新 ...

  10. [VC]在VC++中实现让程序只运行一个实例的方法且实现该实例

    方法一: 有时候在开发应用程序时,希望控制程序运行唯一的实例.例如,最常用的mp3播放软 件Winamp,由于它需要独占计算机中的音频设备,因此该程序只允许自身运行唯一的一个例程.在Visual C+ ...