Vue源码探究-事件系统
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)
}
}
}
这段代码中用到了 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事件监听函数数组,再添加新监听函数
(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<string>, 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 < 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 && 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 > 1 ? toArray(cbs) : cbs
// 将event之后传入的所有参数定义为args数组
const args = toArray(arguments, 1)
// 遍历所有监听函数,为实例执行每一个监听函数,并传入args参数数组
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
}
}
eventsMixin的内容非常直观,分别为实例原型对象挂载了$on
、$once
、$off
、$emit
四个方法。这是实例事件监听函数的注册、一次性注册、移除和触发的内部实现。在使用的过程中会对这些实现有一个更清晰的理解。
终于对Vue的事件系统的实现有了一个大致了解,没有什么特别高深的处理,但完整的事件系统的实现有很多细致的功能这里其实并没有特别详细地探讨,比如事件修饰符,可以参考官方文档里的解说会有一个更清晰的了解。事件系统的重要作用首先是为实例制定了一套处理事件的方案和标准,其次是在实例数据更新的过程中保持对事件监听器的更新,这两个部分的处理是最需要细致去琢磨的。
原文地址:https://segmentfault.com/a/1190000016757343
Vue源码探究-事件系统的更多相关文章
- Vue源码探究-全局API
Vue源码探究-全局API 本篇代码位于vue/src/core/global-api/ Vue暴露了一些全局API来强化功能开发,API的使用示例官网上都有说明,无需多言.这里主要来看一下全局API ...
- Vue源码探究-状态初始化
Vue源码探究-状态初始化 Vue源码探究-源码文件组织 Vue源码探究-虚拟DOM的渲染 本篇代码位于vue/src/core/instance/state.js 继续随着核心类的初始化展开探索其他 ...
- Vue源码探究-源码文件组织
Vue源码探究-源码文件组织 源码探究基于最新开发分支,当前发布版本为v2.5.17-beta.0 Vue 2.0版本的大整改不仅在于使用功能上的优化和调整,整个代码库也发生了天翻地覆的重组.可见随着 ...
- Vue源码探究-数据绑定的实现
Vue源码探究-数据绑定的实现 本篇代码位于vue/src/core/observer/ 在总结完数据绑定实现的逻辑架构一篇后,已经对Vue的数据观察系统的角色和各自的功能有了比较透彻的了解,这一篇继 ...
- Vue源码探究-虚拟DOM的渲染
Vue源码探究-虚拟DOM的渲染 在虚拟节点的实现一篇中,除了知道了 VNode 类的实现之外,还简要地整理了一下DOM渲染的路径.在这一篇中,主要来分析一下两条路径的具体实现代码. 按照创建 Vue ...
- vue 源码详解(二): 组件生命周期初始化、事件系统初始化
vue 源码详解(二): 组件生命周期初始化.事件系统初始化 上一篇文章 生成 Vue 实例前的准备工作 讲解了实例化前的准备工作, 接下来我们继续看, 我们调用 new Vue() 的时候, 其内部 ...
- VUE 源码学习01 源码入口
VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...
- 大白话Vue源码系列(03):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
- 大白话Vue源码系列(04):生成render函数
阅读目录 优化 AST 生成 render 函数 小结 本来以为 Vue 的编译器模块比较好欺负,结果发现并没有那么简单.每一种语法指令都要考虑到,处理起来相当复杂.上篇已经生成了 AST,本篇依然对 ...
随机推荐
- Codeforces 161C(分治、性质)
要点 因为当前最大字符只有一个且两边是回文的,所以如果答案包含最大字符则一定是重合部分. 若不包含,则用此字符将两个区间分别断为两部分,则共有四种组合,答案一定为其中之一. #include < ...
- 2017"百度之星"程序设计大赛 - 初赛(B)小小粉丝度度熊
Problem Description 度度熊喜欢着喵哈哈村的大明星——星星小姐. 为什么度度熊会喜欢星星小姐呢? 首先星星小姐笑起来非常动人,其次星星小姐唱歌也非常好听. 但这都不是最重要的,最重要 ...
- GPU程序缓存(GPU Program Caching)
GPU程序缓存 翻译文章: GPU Program Caching 总览 / 为什么 因为有一个沙盒, 每一次加载页面, 我们都会转化, 编译和链接它的GPU着色器. 当然不是每一个页面都需要着色器, ...
- NET Core中使用Dapper操作Oracle存储过程
.NET Core中使用Dapper操作Oracle存储过程最佳实践 为什么说是最佳实践呢?因为在实际开发中踩坑了,而且发现网上大多数文章给出的解决方法都不能很好地解决问题.尤其是在获取类型为Or ...
- python HTTP 状态码
404 Not Found 在HTTP请求的路径无法匹配任何RequestHandler类相对应的模式时返回404(Not Found)响应码. 400 Bad Request 如果你调用了一个没有默 ...
- linux yum 安装
################## http://rpm.pbone.net/ 下载下来的包放到本地yum源中,然后在这个目录下面重新生成依赖关系就可以使用yum包来完成安装了 tt 1. 生成依赖 ...
- Java程序运行参数
Java主函数形式:public static void main(String[] args){......} 也就是说可以向Java程序传递一个String[]. 1.在IDEA中debug.ru ...
- UIWebView全解
是iOS内置的浏览器控件,可以浏览网页.打开文档等 能够加载html/htm.pdf.docx.txt等格式的文件 系统自带的Safari浏览器就是通过UIWebView实现的 MIME的英文全称是“ ...
- MySql自动默认时间及更新时间
注意:5.7 才能用类型为datetime的字段实现 `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `modifie ...
- Android商城开发系列(一)——开篇
最近在看尚硅谷的硅谷商城视频,想系统学习一下Android的商城开发流程,打算跟着视频的一步步做出一个商城,然后写博客总结记录一下整个商城的开发过程以及使用到的技术知识点,这个商城的最终效果如下图所示 ...