jQuery 源码分析(十六) 事件系统模块 底层方法 详解
jQuery事件系统并没有将事件监听函数直接绑定到DOM元素上,而是基于数据缓存模块来管理监听函数的,事件模块代码有点多,我把它分为了三个部分:分底层方法、实例方法和便捷方法、ready事件来讲,好理解一点。
jQuery的事件分为普通事件和代理事件:
- 普通事件 ;当我们再div上定义一个click事件,此时如果点击div或按钮都会触发该普通事件,这是由于冒泡的缘故
- 代理事件 ;当我们在div上定义一个代理事件,且selector设置为button时,我们点击div将不会触发该事件,只有点击了这个按钮才会触发这个代理事件
事件系统模块的底层方法如下:
- $.event.add(elem,types,handler,data,selector) ;绑定一个或多个类型的事件监听函数,参数如下:
- elem ;操作的元素
- types ;绑定的事件类型,多个事件类型之间用空格隔开。
- handler ;待绑定的事件监听函数,也可以是一个自定义的监听对象。
- data ;自定义数据。
- selector ;选择器表达式字符串,用于绑定代理事件。 ;
- $.event.remove(elem, types, handler, selector, mappedTypes) ;移除DOM元素上绑定的一个或多个类型的事件监听函数,参数同$.event.add()。如果只传入一个elem元素则移除该元素上的所有事件。
- $.event.trigger(type,data,elem,onlyHandlers) ;手动触发事件,执行绑定的事件监听函数和默认行为,并且会模拟冒泡过程。参数如下:
- type ;事件字符串
- data ;传递给响应函数的数据
- elem ;触发该事件的DOM对象
- onlyHandlers ;是否只触发elem元素对应的事件监听函数,而不冒泡
常用的就是以上三个吧,其它还有$.event.global(记录绑定过的事件)、$.event.removeEvent(用于删除事件)等
先举栗子前先先简单说一下jQuery里的事件的分类,jQuery的事件分为普通事件和代理事件:
- 普通事件 ;直接绑定在这个元素的某个事件类型上,当在该元素上触发了这个事件时,则执行该事件
- 代理事件 ;当事件直接发生在代理元素上时,监听函数不会执行,只有当事件从后代元素冒泡到代理元素上时,才会用参数selector匹配冒泡路上的后代元素,然后用匹配成功的后代元素作为上下文去执行监听函数。
这样说可能理解不了,举个栗子,我们定义两个DOM元素先
这里我们定义了一个div,内部含有一个子节点button,渲染如下:
举个栗子:
writer by:大沙漠 QQ:22969969
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script src="http://libs.baidu.com/jquery/1.7.1/jquery.min.js"></script>
<style>div{width: 200px;padding-top:50px;height: 150px;background: #ced;}div button{margin:0 auto;display: block;}</style>
</head>
<body>
<div>
<button id="button">按钮1</button>
</div>
<script>
let div = document.getElementsByTagName('div')[0],
btn = document.getElementsByTagName('button')[0]; $.event.add(div,'click',()=>console.log('div普通单击事件')); //给div绑定一个click事件
$.event.add(div,'click',()=>console.log('d1代理事件'),null,'button'); //给div绑定一个代理事件,监听对象为button
</script>
</body>
</html>
渲染如下:
我们给div绑定了一个普通事件(类型为click),还有一个代理事件(代理的元素是button),当我们点击div元素时将触发普通事件,如下:
代理事件并没有被触发,当我们点击按钮1时将会同时触发普通事件和代理事件:
这里的代理事件是在jQuery内部实现的,而普通事件是因为原生的冒泡的事件流所产生的,
源码分析
jQuery内部的事件绑定也是通过原生的addEventListener或attachEvent来实现的,不过jQuery对这个过程做了优化,它不只是仅仅的调用该API实现绑定,用jQuery绑定事件时,在同一个DOM元素上的绑定的任何事件,其实最后绑定的都是同一个函数,该函数只有几行diamagnetic,最后又会调用jQuery.event.dispatch去进行事件的分发,并执行事件监听函数。
上面说了事件系统模块是基于数据缓存模块来管理监听函数的,我们通过jQuery绑定的事件都保存到了$. cache里对应的DOM元素的数据缓存对象上(有疑问的可以看下数据缓存模块,前面介绍过了),比如上面的栗子,我们打印一下$.cache可以看到绑定的信息:
每个DOM元素在内部数据缓存对上有两个属性是和事件有关的:
- events ;属性是一个对象,其中存储了该DOM元素的所有事件,该对象的下的每个元素的元素名是事件类型,值是一个数组,是封装了监听函数的handleObject集合
- handle ;DOM元素的主监听函数,负责分发事件和执行监听函数,对于一个DOM元素,jQuery事件系统只会为之分配一个主监听函数,所有类型的事件都被绑定到这个主监听函数
好了,现在来说一下源码实现,$.event.add的源码实现如下:
jQuery.event = {
add: function( elem, types, handler, data, selector ) { //绑定一个或多个类型的事件监听函数 var elemData, eventHandle, events,
t, tns, type, namespaces, handleObj,
handleObjIn, quick, handlers, special; // Don't attach events to noData or text/comment nodes (allow plain objects tho)
if ( elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data( elem )) ) { //排除文本节点(浏览器不会在文本节点上触发事件)、注释节点(没有意义)、参数不完整的情况
return;
} // Caller can pass in an object of custom data in lieu of the handler
if ( handler.handler ) { //如果参数handle是自定义监听对象,其中的属性价会被设置到后面所创建的新监听对象上。函数cloneCopyEvent(src,dest)将调用该方法
handleObjIn = handler;
handler = handleObjIn.handler;
} // Make sure that the handler has a unique ID, used to find/remove it later
if ( !handler.guid ) { //如果函数handler没有guid
handler.guid = jQuery.guid++; //则为它分配一个唯一标识guid,在移除监听函数时,将通过这个唯一标识来匹配监听函数。
} // Init the element's event structure and main handler, if this is the first
events = elemData.events; //尝试取出事件缓存对象
if ( !events ) { //如果不存在,表示从未在当前元素上(通过jQuery事件方法)绑定过事件,
elemData.events = events = {}; //则把它初始化为一个空对象。events对象保存了存放当前元素关联的所有监听函数。
}
eventHandle = elemData.handle; //尝试取出主监听函数handle(event)
if ( !eventHandle ) {
elemData.handle = eventHandle = function( e ) { //如果不存在,表示从未在当前元素上(jQuery事件方法)绑定过事件,则初始化一个主监听函数,并把它存储到事件缓存对象的handle属性上,这是事件触发时真正执行的函数
// Discard the second event of a jQuery.event.trigger() and
// when an event is called after a page has unloaded
return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ?
jQuery.event.dispatch.apply( eventHandle.elem, arguments ) :
undefined;
};
// Add elem as a property of the handle fn to prevent a memory leak with IE non-native events
eventHandle.elem = elem;
} // Handle multiple events separated by a space
// jQuery(...).bind("mouseover mouseout", fn);
types = jQuery.trim( hoverHack(types) ).split( " " ); //先调用hoverHack()函数修正hover.namespace类型的事件,再调用split把types用空格进行分隔,转换为一个数组
for ( t = 0; t < types.length; t++ ) { //遍历需要绑定的每个事件类型,逐个绑定事件 tns = rtypenamespace.exec( types[t] ) || []; //正则rtypenamespace用于解析事件类型和命名空间,执行后tns[1]是事件类型,tns[2]是一个或多个命名空间,用.分隔
type = tns[1]; //type就是事件类型
namespaces = ( tns[2] || "" ).split( "." ).sort(); // If event changes its type, use the special event handlers for the changed type
special = jQuery.event.special[ type ] || {}; // If selector defined, determine special event api type, otherwise given type
type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type
special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers
handleObj = jQuery.extend({ //把监听函数封装为监听对象,用来支持事件模拟、自定义事件数据等。
type: type, //实际使用的事件类型,不包含命名空间,可能被修改过
origType: tns[1], //原始事件类型,不包含命名空件,未经过修正
data: data, //排序后的命名空间,如果传入的事件类型是'click.a.c.b',那么namespace就是a.b.c
handler: handler, //传入的监听函数
guid: handler.guid, //分配给监听函数的唯一标识guid
selector: selector, //自定义的事件数据
quick: quickParse( selector ), //入的事件代理选择器表达式,当代理事件被触发时,用该属性过滤代理元素的后代元素。
namespace: namespaces.join(".")
}, handleObjIn ); // Init the event handler queue if we're the first
handlers = events[ type ]; //尝试取出事件类型type对应的监听对象数组handlers,其中存放了已绑定的监听对象。
if ( !handlers ) { //如果type事件类型的数组不存在,则进行绑定操作
handlers = events[ type ] = []; //把监听对象数组的handlers初始化为一个空数组。
handlers.delegateCount = 0; //初始化handlers.delegateCount为0,表示代理事件的个数为0 // Only use addEventListener/attachEvent if the special events handler returns false
if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { //绑定主监听函数 优先调用修正对象的修正方法setup()
// Bind the global event handler to the element
if ( elem.addEventListener ) { //如果浏览器支持addEventListener()方法
elem.addEventListener( type, eventHandle, false ); //调用原生方法addEventListener()绑定主监听函数,以冒泡流的方式。 } else if ( elem.attachEvent ) { //如果没有addEventListener()
elem.attachEvent( "on" + type, eventHandle ); //则调用attachEvent()方法绑定主监听函数,IE8及更早的浏览器只支持冒泡
}
}
} if ( special.add ) { //如果修正对象有修正方法add()
special.add.call( elem, handleObj ); //则先调用修正方法add()绑定监听函数 if ( !handleObj.handler.guid ) {
handleObj.handler.guid = handler.guid;
}
} // Add to the element's handler list, delegates in front
if ( selector ) { //如果传入了selector参数,则绑定的是代理事件
handlers.splice( handlers.delegateCount++, 0, handleObj ); //把代理监听对象插入属性handlers.delegateCount所指定的位置。每次插入代理监听对象后,监听对象数组的属性handlers.delegateCount自动加1,以指示下一个代理监听对象的插入位置。
} else { //未传入selector参数情况下,则是普通的事件绑定
handlers.push( handleObj ); //把监听对象插入末尾
} // Keep track of which events have ever been used, for event optimization
jQuery.event.global[ type ] = true; //记录绑定过的事件类型
} // Nullify elem to prevent memory leaks in IE
elem = null; //解除参数elem对DOM元素的引用,以避免内存泄漏(IE)
},
/*略*/
}
$.event.add会通过addEventListener或attachEvent去绑定事件,当事件被触发时就会执行我们绑定的事件,也就是上面的elemData.handle函数,该函数会执行jQuery.event.dispatch函数,jQuery.event.dispatch就是用于分发事件的,如下:
jQuery.event = {
dispatch: function( event ) { //分发事件,执行事件监听函数 // Make a writable jQuery.Event from the native event object
event = jQuery.event.fix( event || window.event ); //调用jQuery.event.fix(event)把原生事件对象封装为jQuery事件对象。 var handlers = ( (jQuery._data( this, "events" ) || {} )[ event.type ] || []), //取出this元素当前事件类型对应的函数列表。
delegateCount = handlers.delegateCount, //代理监听对象个事
args = [].slice.call( arguments, 0 ), //把参数arguments转换为真正的数组
run_all = !event.exclusive && !event.namespace,
handlerQueue = [],
i, j, cur, jqcur, ret, selMatch, matched, matches, handleObj, sel, related; // Use the fix-ed jQuery.Event rather than the (read-only) native event
args[0] = event; //将event保存为args[0],监听函数执行时这是作为第一个参数传入的
event.delegateTarget = this; //当前的DOM对象,等于event.currentTarget属性的值 // Determine handlers that should run if there are delegated events
// Avoid disabled elements in IE (#6911) and non-left-click bubbling in Firefox (#3861)
if ( delegateCount && !event.target.disabled && !(event.button && event.type === "click") ) { //如果当前对象绑定了代理事件,且目标对象target没有禁用 !(event.button && event.type === "click")的意思是:是鼠标单击时 // Pregenerate a single jQuery object for reuse with .is()
jqcur = jQuery(this); //用当前元素构造一个jQuery对象,以便在后面的代码中复用它的方法.is(selector)
jqcur.context = this.ownerDocument || this; //上下文 for ( cur = event.target; cur != this; cur = cur.parentNode || this ) { //从事件目标开始,再依次到父节点,一直到当前目标为止,遍历从触发事件的元素到代理元素这条路径上的所有后代元素。
selMatch = {}; //重置selMatch为空对象
matches = []; //重置matches为空数组
jqcur[0] = cur; //将jqcur绑定到每一个后代元素
for ( i = 0; i < delegateCount; i++ ) { //遍历所有的代理监听对象数组。
handleObj = handlers[ i ]; //某个代理监听对象。
sel = handleObj.selector; //当前代理监听对象的selector属性 if ( selMatch[ sel ] === undefined ) { //如果绑定的事件对应的选择器在selMatch中不存在,则进行检查,看是否符合要求
selMatch[ sel ] = (
handleObj.quick ? quickIs( cur, handleObj.quick ) : jqcur.is( sel )
);
}
if ( selMatch[ sel ] ) { //当后代元素与代理监听对象的选择器表达式匹配时
matches.push( handleObj ); //把代理监听对象放入数组matches中。
}
}
if ( matches.length ) {
handlerQueue.push({ elem: cur, matches: matches }); //最后把某个后代元素匹配的所有代理监听对象代理放到数组handlerQueue里。
}
}
} // Add the remaining (directly-bound) handlers
if ( handlers.length > delegateCount ) { //如果监听对象数组handlers的长度大于代理监听对象的位置计数器delegateCount,则表示为当前元素绑定了普通事件
handlerQueue.push({ elem: this, matches: handlers.slice( delegateCount ) }); //把这些监听对象也放入待执行队列handlerQueue中
} // Run delegates first; they may want to stop propagation beneath us
for ( i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++ ) { //执行数组handlerQueue中的所有函数。遍历待执行队列handlerQueue,如果某个元素的监听函数调用了方法stopPropagation()则终止for循环
matched = handlerQueue[ i ]; //matched是数组handlerQueue中的一个对象
event.currentTarget = matched.elem; //把当前正在执行监听函数的元素赋值给事件属性event.currentTarget。这样在事件处理函数内,this等于绑定的代理函数 for ( j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++ ) { //遍历元素定对应的监听对象数组,并执行监听对象中的监听函数
handleObj = matched.matches[ j ]; //handleObj是一个具体的监听对象handleObj // Triggered event must either 1) be non-exclusive and have no namespace, or
// 2) have namespace(s) a subset or equal to those in the bound event (both can have no namespace).
if ( run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test( handleObj.namespace ) ) { event.data = handleObj.data; //把监听对象的属性handleObj.data赋值给jQuery事件对象
event.handleObj = handleObj; //把监听对象赋值给jQuery事件对象event.handleObj上,这样监听函数就可以通过该属性访问监听对象上的诸多属性 ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) //优先调用修正对象的修正方法special.handle(),
.apply( matched.elem, args ); if ( ret !== undefined ) { //如果函数有返回值
event.result = ret;
if ( ret === false ) { //如果监听对象的返回值是false
event.preventDefault(); //调用preventDefault()方法阻止默认行为
event.stopPropagation(); //stopPropagation()方法停止事件传播
}
}
}
}
} return event.result;
},
/*略*/
}
jQuery.event.fix()会把原生事件修正为jQuery事件对象并返回,修正不兼容属性,就是自定义一个对象,保存该事件的信息,比如:类型、创建的时间、原生事件对象等,有兴趣的可以调试一下。
jQuery 源码分析(十六) 事件系统模块 底层方法 详解的更多相关文章
- jQuery 源码分析(十三) 数据操作模块 DOM属性 详解
jQuery的属性操作模块总共有4个部分,本篇说一下第2个部分:DOM属性部分,用于修改DOM元素的属性的(属性和特性是不一样的,一般将property翻译为属性,attribute翻译为特性) DO ...
- Vue.js 源码分析(十六) 指令篇 v-on指令详解
可以用 v-on 指令监听 DOM 事件,并在触发时运行一些 JavaScript 代码,例如: <!DOCTYPE html> <html lang="en"& ...
- jQuery 源码分析(十) 数据缓存模块 data详解
jQuery的数据缓存模块以一种安全的方式为DOM元素附加任意类型的数据,避免了在JavaScript对象和DOM元素之间出现循环引用,以及由此而导致的内存泄漏. 数据缓存模块为DOM元素和JavaS ...
- Vue.js 源码分析(十五) 指令篇 v-bind指令详解
指令是Vue.js模板中最常用的一项功能,它带有前缀v-,比如上面说的v-if.v-html.v-pre等.指令的主要职责就是当其表达式的值改变时,相应的将某些行为应用到DOM上,先介绍v-bind指 ...
- Vue.js 源码分析(十八) 指令篇 v-for 指令详解
我们可以用 v-for 指令基于一个数组or对象来渲染一个列表,有五种使用方法,如下: <!DOCTYPE html> <html lang="en"> & ...
- jQuery 源码解析(三十一) 动画模块 便捷动画详解
jquery在$.animate()这个接口上又封装了几个API,用于进行匹配元素的便捷动画,如下: $(selector).show(speed,easing,callback) ;如 ...
- jQuery 源码分析(十二) 数据操作模块 html特性 详解
jQuery的属性操作模块总共有4个部分,本篇说一下第1个部分:HTML特性部分,html特性部分是对原生方法getAttribute()和setAttribute()的封装,用于修改DOM元素的特性 ...
- jQuery 源码分析(十九) DOM遍历模块详解
jQuery的DOM遍历模块对DOM模型的原生属性parentNode.childNodes.firstChild.lastChild.previousSibling.nextSibling进行了封装 ...
- jQuery 源码分析(十五) 数据操作模块 val详解
jQuery的属性操作模块总共有4个部分,本篇说一下最后一个部分:val值的操作,也是属性操作里最简单的吧,只有一个API,如下: val(vlaue) ;获取匹配元素集合中第一个元素的 ...
随机推荐
- 创建可执行的JAR包并运行
将一个应用程序制作成可执行的JAR包,通过JAR包来发布应用程序.创建可执行JAR包的关键在于:让java -jar命令知道JAR包中哪个类是主类,java -jar命令可以通过运行该主类来运行程序. ...
- jmeter 中使用正则表达式提取依赖参数
1:登录接口 这里有一个实际的登录接口,在响应中返回了一串token,如下图 那么我们在接下来的接口-经验库列表中,就必须带入这一串token,否则响应报错,如下图所示 如何获取登录的口令呢?这 ...
- SpringBoot2 整合 ClickHouse数据库,实现高性能数据查询分析
本文源码:GitHub·点这里 || GitEE·点这里 一.ClickHouse简介 1.基础简介 Yandex开源的数据分析的数据库,名字叫做ClickHouse,适合流式或批次入库的时序数据.C ...
- 多进程操作-进程队列multiprocess.Queue的使用
一.ipc机制 进程通讯 管道:pipe 基于共享的内存空间 队列:pipe+锁 queue 下面拿代码来实现Queue如何使用: 案例一: from multiprocessing import Q ...
- 原生js对cookie的增删改查
一.增 document.cookie = cname + "=" + cvalue + ";expires=" + expires + ";path ...
- Add an Action with Option Selection 添加具有选项选择的按钮
In this lesson, you will learn how to create an Action with support for option selection. A new View ...
- 关于如何获取项目所部署的本机IP和端口的问题
关于如何获取项目所部署的本机IP和端口的问题 今天在写一个需求的时候碰到一个不常见的问题,在没有继承或者实现服务器提供的接口或者实现类的时候,比如说部署在tomacat上,某个类不去继承servelt ...
- CSS学习笔记-盒子阴影及文字阴影
盒子阴影: 1.格式: box-shadow:h-shadow v-shadow blur spread color insert; box-shadow:水平偏移 ...
- 10分钟浅谈CSRF突破原理,Web安全的第一防线!
CSRF攻击即跨站请求伪造(跨站点请求伪造),是一种对网站的恶意利用,听起来似乎与XSS跨站脚本攻击有点相似,但实际上彼此相差很大,XSS利用的是站点内的信任用户,而CSRF则是通过伪装来自受信任用户 ...
- 通过 RxSwift 优雅使用 NotificationCenter
原文 纯粹的官方代码使用NotificationCenter真的很难用,但是有了RxSwift,就变得方便了很多. 修改 Podfile,通过pod引入RxSwift pod 'RxSwift' po ...