说起jQuery的事件,不得不提一下Dean Edwards大神 addEvent库,很多流行的类库的基本思想从他那儿借来的
jQuery的事件处理机制吸取了JavaScript专家Dean Edwards编写的事件处理函数的精华,使得jQuery处理事件绑定的时候相当的可靠。
在预留退路(graceful degradation),循序渐进以及非入侵式编程思想方面,jQuery也做的非常不错
事件的流程图
总的来说对于JQuery的事件绑定
在绑定的时候做了包装处理
在执行的时候有过滤器处理
.on( events [, selector ] [, data ], handler(eventObject) )
events:事件名
selector : 一个选择器字符串,用于过滤出被选中的元素中能触发事件的后代元素
data :当一个事件被触发时,要传递给事件处理函数的
handler:事件被触发时,执行的函数
例如:
var body = $('body') body.on('click','p',function(){ console.log(this) })
用on方法给body上绑定一个click事件,冒泡到p元素的时候才出发回调函数
这里大家需要明确一点:每次在body上点击其实都会触发事件,但是只目标为p元素的情况下才会触发回调handler
通过源码不难发现,on方法实质只完成一些参数调整的工作,而实际负责事件绑定的是其内部jQuery.event.add方法


jQuery.onon: function( types, selector, data, fn, /*INTERNAL*/ one ) { var origFn, type; // Types can be a map of types/handlers if ( typeof types === "object" ) { // ( types-Object, selector, data ) if ( typeof selector !== "string" ) { // ( types-Object, data ) data = data || selector; selector = undefined; } for ( type in types ) { this.on( type, selector, data, types[ type ], one ); } return this; } if ( data == null && fn == null ) { // ( types, fn ) fn = selector; data = selector = undefined; } else if ( fn == null ) { if ( typeof selector === "string" ) { // ( types, selector, fn ) fn = data; data = undefined; } else { // ( types, data, fn ) fn = data; data = selector; selector = undefined; } } if ( fn === false ) { fn = returnFalse; } else if ( !fn ) { return this; } if ( one === 1 ) { origFn = fn; fn = function( event ) { // Can use an empty set, since event contains the info jQuery().off( event ); return origFn.apply( this, arguments ); }; // Use same guid so caller can remove using origFn fn.guid = origFn.guid || ( origFn.guid = jQuery.guid++ ); } return this.each( function() { jQuery.event.add( this, types, fn, data, selector ); });
针对事件处理,我们可以拆分2部分:
一个事件预绑定期
一个事件执行期
本章着重讲解事件的预绑定的时候做了那些处理,为什么要这样处理?
事件底层的绑定接口无非就是用addEventListener处理的,所以我们直接定位到addEventListener下面
jQuery.event.add 中有
elem: 目标元素
type: 事件类型,如’click’
eventHandle: 事件句柄,也就是事件回调处理的内容了
false: 冒泡
现在我们把之前的案例给套一下看看
var body = document.getElementsByTagName('body') var eventHandle = function(){ console.log(this) } body .addEventListener( ‘click’, eventHandle, false );
明显有问题,每次在body上都触发了回调,少了个p元素的处理,当然这样的效果也无法处理
eventHandle源码
回到内部绑定的事件句柄eventHandle ,可想而知eventHandle不仅仅只是只是充当一个回调函数的角色,而是一个实现了EventListener接口的对象
if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== core_strundefined && (!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; }
可见在eventHandle中并没有直接处理回调函数,而是映射到jQuery.event.dispatch分派事件处理函数了
仅仅只是传入eventHandle.elem,arguments , 就是body元素 与事件对象
那么这里有个问题,事件回调的句柄并没有传递过去,后面的代码如何关联?
本章的一些地方可能要结合后面的dispatch处理才能理清,但是我们还是先看看做了那些处理
on内部的实现机制
我们开从头来理清下jQuery.event.add代码结构,适当的跳过这个环节中不能理解的代码,具体遇到在提出
之前就提到过jQuery从1.2.3版本引入数据缓存系统,贯穿内部,为整个体系服务,事件体系也引入了这个缓存机制
所以jQuery并没有将事件处理函数直接绑定到DOM元素上,而是通过 .data存储在缓存 .cahce上
第一步:获取数据缓存
//获取数据缓存 elemData = data_priv.get( elem );
在$.cahce缓存中获取存储的事件句柄对象,如果没就新建elemData
第二步:创建编号
if ( !handler.guid ) {
handler.guid = jQuery.guid++;
}
为每一个事件的句柄给一个标示,添加ID的目的是 用来寻找或者删除handler,因为这个东东是缓存在缓存对象上的,没有直接跟元素节点发生关联
第三步:分解事件名与句柄
if ( !(events = elemData.events) ) { events = elemData.events= {}; } if ( !(eventHandle = elemData.handle) ) { eventHandle = elemData.handle = function( e ) { // Discard the second event of a jQuery.event.trigger() and // when an event is called after a page has unloaded return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; }; eventHandle.elem = elem; }
events,eventHandle 都是elemData缓存对象内部的,可见
在elemData中有两个重要的属性,
一个是events,是jQuery内部维护的事件列队
一个是handle,是实际绑定到elem中的事件处理函数
之后的代码无非就是对这2个对象的筛选,分组,填充了
第四步: 填充事件名与事件句柄
// Handle multiple events separated by a space // jQuery(...).bind("mouseover mouseout", fn); // 事件可能是通过空格键分隔的字符串,所以将其变成字符串数组 // core_rnotwhite:/\S+/g types = ( types || "" ).match( core_rnotwhite ) || [""]; // 例如:'.a .b .c'.match(/\S+/g) → [".a", ".b", ".c"] // 事件的个数 t = types.length; while ( t-- ) { // 尝试取出事件的命名空间 // 如"mouseover.a.b" → ["mouseover.a.b", "mouseover", "a.b"] tmp = rtypenamespace.exec( types[t] ) || []; // 取出事件类型,如mouseover type = origType = tmp[1]; // 取出事件命名空间,如a.b,并根据"."分隔成数组 namespaces = ( tmp[2] || "" ).split( "." ).sort(); // There *must* be a type, no attaching namespace-only handlers if ( !type ) { continue; } // 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 // 根据是否已定义selector,决定使用哪个特殊事件api,如果没有非特殊事件,则用type type = ( selector ? special.delegateType : special.bindType ) || type; // Update special based on newly reset type // type状态发生改变,重新定义特殊事件 special = jQuery.event.special[ type ] || {}; // handleObj is passed to all event handlers // 这里把handleObj叫做事件处理对象,扩展一些来着handleObjIn的属性 handleObj = jQuery.extend({ type: type, origType: origType, data: data, handler: handler, guid: handler.guid, selector: selector, needsContext: selector && jQuery.expr.match.needsContext.test( selector ), namespace: namespaces.join(".") }, handleObjIn ); // Init the event handler queue if we're the first // 初始化事件处理列队,如果是第一次使用,将执行语句 if ( !(handlers = events[ type ]) ) { handlers = events[ type ] = []; handlers.delegateCount = 0; // Only use addEventListener if the special events handler returns false // 如果获取特殊事件监听方法失败,则使用addEventListener进行添加事件 if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } } } // 特殊事件使用add处理 if ( special.add ) { special.add.call( elem, handleObj ); // 设置事件处理函数的ID if ( !handleObj.handler.guid ) { handleObj.handler.guid = handler.guid; } } // Add to the element's handler list, delegates in front // 将事件处理对象推入处理列表,姑且定义为事件处理对象包 if ( selector ) { handlers.splice( handlers.delegateCount++, 0, handleObj ); } else { 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 // 设置为null避免IE中循环引用导致的内存泄露 elem = null; },
这段比较长了分解下,最终的目的就是为填充events,eventHandle
涉及
多事件处理:
如果是多事件分组的情况jQuery(...).bind("mouseover mouseout", fn);
事件可能是通过空格键分隔的字符串,所以将其变成字符串数组
增加命名空间处理:
事件名称可以添加指定的event namespaces(命名空间) 来简化删除或触发事件。例如,"click.myPlugin.simple"
为 click 事件同时定义了两个命名空间 myPlugin 和 simple。通过上述方法绑定的 click 事件处理,可以用.off("click.myPlugin")
或 .off("click.simple")
删除绑定到相应元素的Click事件处理程序,而不会干扰其他绑定在该元素上的“click(点击)” 事件。命名空间类似CSS类,因为它们是不分层次的;只需要有一个名字相匹配即可。以下划线开头的名字空间是供 jQuery 使用的。
引入jQuery的Special Event机制
什么时候要用到自定义函数?有些浏览器并不兼容某类型的事件,如IE6~8不支持hashchange事件,你无法通过jQuery(window).bind('hashchange', callback)来绑定这个事件,这个时候你就可以通过jQuery自定义事件接口来模拟这个事件,做到跨浏览器兼容。
原理
jQuery(elem).bind(type, callbakc)实际上是映射到 jQuery.event.add(elem, types, handler, data)这个方法,每一个类型的事件会初始化一次事件处理器,而传入的回调函数会以数组的方式缓存起来,当事件触发的时候处理器将依次执行这个数组。
jQuery.event.add方法在第一次初始化处理器的时候会检查是否为自定义事件,如果存在则将会把控制权限交给自定义事件的事件初始化函数,同样事件卸载的jQuery.event.remove方法在删除处理器前也会检查此。
!special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false
jQuery.removeEvent( elem, type, elemData.handle );
jQuery.event.special对象中,保存着为适配特定事件所需的变量和方法,
具体有:
delegateType / bindType (用于事件类型的调整)
setup (在某一种事件第一次绑定时调用)
add (在事件绑定时调用)
remove (在解除事件绑定时调用)
teardown (在所有事件绑定都被解除时调用)
trigger (在内部trigger事件的时候调用)
noBubble
_default
handle (在实际触发事件时调用)
preDispatch (在实际触发事件前调用)
postDispatch (在实际触发事件后调用)
在适配工作完成时,会产生一个handleObj对象,这个对象包含了所有在事件实际被触发是所需的所有参数
采用自定义事件或者浏览器接口绑定事件
if ( !special.setup || special.setup.call( elem, data, namespaces, eventHandle ) === false ) { if ( elem.addEventListener ) { elem.addEventListener( type, eventHandle, false ); } }
冒泡标记
handlers.splice( handlers.delegateCount++, 0, handleObj );
最后记得
设置为null避免IE中循环引用导致的内存泄露
elem = null;
这个元素没有直接让事件直接引用了,而是挂在到,数据缓存句柄上,很好的避免了这个IE泄露的问题
eventHandle.elem = elem;
通过整个流程,我们的数据缓存对象就填充完毕了,看看截图
events:handleObj
handle
数据缓存对象
得出总结:
在jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : 方法中没有传递回调对象
是因为回调的句柄被关联到了elemData,也就是内部数据缓存中了
不难得出jQuery的事件绑定机制:
jQuery对每一个elem中的每一种事件,只会绑定一次事件处理函数(绑定这个elemData.handle),
而这个elemData.handle实际只做一件事,就是把event丢到jQuery内部的事件分发程序
jQuery.event.dispatch.apply( eventHandle.elem, arguments );
而不同的事件绑定,具体是由jQuery内部维护的事件列队来区分(就是那个elemData.events)
在elemData中获取到events和handle之后,接下来就需要知道这次绑定的是什么事件了
画了个简单流程图
从上章就能得出几个信息:
- 事件信息都存储在数据缓存中
- 对于没有特殊事件特有监听方法和普通事件都用addEventListener来添加事件了。
- 而又特有监听方法的特殊事件,则用了另一种方式来添加事件。
本章分析的重点:
通过addEventListener触发事件后,回调句柄如何处理?
具体来说就是,如何委派事件的,用到哪些机制,我们如果用到项目上是否能借鉴?
涉及的处理:
事件句柄的读取与处理
事件对象的兼容,jQuery采取什么方式处理?
委托关系的处理
jQuery引入的处理方案
jQuery.event.fix(event):将原生的事件对象 event 修正为一个 可以读读写event 对象,并对该 event 的属性以及方法统一接口。
jQuery.Event(event,props): 构造函数创建可读写的 jQuery事件对象 event, 该对象即可以是原生事件对象 event 的增强版,也可以是用户自定义事件
jQuery.event.handlers: 用来区分原生与委托事件
能学到的思路
缓存的分离
适配器模式的运用
事件兼容性的封装
委托的设计
事件的绑定执行顺序
结构
<div id='p1' style="width: 500px;height: 500px;background: #ccc"> <div id='p2' style="width: 300px;height: 300px;background: #a9ea00"> <p id="p3" style="width: 100px;height: 100px;background: red" id = "test"> <a id="p4" style="width: 50px;height: 50px;background:blue" id = "test">点击a元素</a> </p> </div> </div>
假如每一个节点都绑定了事件,那么事件的触发顺序如下:
由此可见:
默认的触发循序是从事件源目标元素也就是event.target指定的元素,一直往上冒泡到document或者body,途经的元素上如果有对应的事件都会被依次触发
如果遇到委托处理?
看demo
最后得到的结论:
元素本身绑定事件的顺序处理机制
分几种情况:
假设绑定事件元素本身是A,委派元素B.C
第一种:
A,B,C各自绑定事件, 事件按照节点的冒泡层次触发
第二种:
元素A本身有事件,元素还需要委派元素B.C事件
委派的元素B.C肯定是该元素A内部的,所以先处理内部的委派,最后处理本身的事件
第三种:
元素本身有事件,元素还需要委派事件,内部委派的元素还有自己的事件,这个有点绕
先执行B,C自己本身的事件,然后处理B,C委派的事件,最后处理A事件
为什么需要了解这个处理的顺序呢? 因为jQuery做委托排序的时候要用到
既然可以冒泡,相应的也应该可以停止
事件对象提供了preventDefault,stopPropagation2个方法一个停止事件传播,一个传递默认的行为(暂且无视IE)
jQuery提供了个万能的 return false 不仅可以阻止事件冒泡,还可以阻止浏览器的默认行为,还可以减少ie系列的bug。
其实就是根据返回的布尔值调用preventDefault,stopPropagation方法,下面会提到
e.stopImmediatePropagation方法不仅阻止了一个事件的冒泡,也把这个元素上的其他绑定事件也阻止了
事件委托原理都知道,但是能有多少写得出jQuery这样的设计思路呢?好吧,如果您觉得不需要,那么看看总是没坏处的。。。
先看看jQuery需要应对的几个问题
需要处理的的问题一:事件对象不同浏览器的兼容性
event 对象是 JavaScript 中一个非常重要的对象,用来表示当前事件。event 对象的属性和方法包含了当前事件的状态。
当前事件,是指正在发生的事件;状态,是与事件有关的性质,如 引发事件的DOM元素、鼠标的状态、按下的键等等。
event 对象只在事件发生的过程中才有效。
浏览器的实现差异:
获取event对象
- 在 W3C 规范中,event 对象是随事件处理函数传入的,Chrome、FireFox、Opera、Safari、IE9.0及其以上版本都支持这种方式;
- 但是对于 IE8.0 及其以下版本,event 对象必须作为 window 对象的一个属性。
- 在遵循 W3C 规范的浏览器中,event 对象通过事件处理函数的参数传入。
- event的某些属性只对特定的事件有意义。比如,fromElement 和 toElement 属性只对 onmouseover 和 onmouseout 事件有意义。
特别指出:分析的版本是2.0.3,已经不再兼容IE6-7-8了,所以部分兼容问题都已经统一了,例如:事件绑定的接口,事件对象的获取等等
事件对象具体有些什么方法属性参照 http://www.itxueyuan.org/view/6340.html
jQuery为dom处理而生,那么处理兼容的手段自然是独树一帜了,所以jQuery对事件的对象的兼容问题单独抽象出一个类,用来重写这个事件对象
jQuery 利用 jQuery.event.fix() 来解决跨浏览器的兼容性问题,统一接口。
除该核心方法外,统一接口还依赖于 (jQuery.event) props、 fixHooks、keyHooks、mouseHooks 等数据模块。
props 存储了原生事件对象 event 的通用属性
keyHook.props 存储键盘事件的特有属性
mouseHooks.props 存储鼠标事件的特有属性。
keyHooks.filter 和 mouseHooks.filter 两个方法分别用于修改键盘和鼠标事件的属性兼容性问题,用于统一接口。
比如 event.which 通过 event.charCode 或 event.keyCode 或 event.button 来标准化。
最后 fixHooks 对象用于缓存不同事件所属的事件类别,比如
fixHooks['click'] === jQuery.event.mouseHooks;
fixHooks['keydown'] === jQuery.event.keyHooks;
fixHooks['focusin'] === {};
从源码处获取对事件对象的操作,调用jQuery.Event重写事件对象
// 将浏览器原生Event的属性赋值到新创建的jQuery.Event对象中去 event = new jQuery.Event( originalEvent );
event就是对原生事件对象的一个重写了,为什么要这样,JQuery要增加自己的处理机制呗,这样更灵活,而且还可以传递data数据,也就是用户自定义的数据
先看看源码,如何处理事件对象兼容?
jQuery.Event构造函数
jQuery.Event = function( src, props ) { if ( src && src.type ) { this.originalEvent = src; this.type = src.type; this.isDefaultPrevented = ( src.defaultPrevented || src.getPreventDefault && src.getPreventDefault() ) ? returnTrue : returnFalse; } else { this.type = src; } if ( props ) {jQuery.extend( this, props );} this.timeStamp = src && src.timeStamp || jQuery.now(); this[ jQuery.expando ] = true; };
方法
jQuery.Event.prototype = { isDefaultPrevented: returnFalse, isPropagationStopped: returnFalse, isImmediatePropagationStopped: returnFalse, preventDefault: function() { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; if ( e && e.preventDefault ) {e.preventDefault();} }, stopPropagation: function() { var e = this.originalEvent; this.isPropagationStopped = returnTrue; if ( e && e.stopPropagation ) {e.stopPropagation(); } }, stopImmediatePropagation: function() { this.isImmediatePropagationStopped = returnTrue; this.stopPropagation(); } };
大体过目下,有个大概的轮毂,后面用了在具体分析
构造出来的新对象
看图,通过jQuery.Event构造器,仅仅只有一些定义的属性与方法,但是原生的事件对象的属性是不是丢了?
所以还需要把原生的的属性给混入到这个新对象上
那么此时带来一个问题,不同事件会产生了不同的事件对象,拥有不同的属性,所以还的有一套适配的机制,根据不同的触发点去适配需要混入的属性名
扩展通过jQuery.Event构造出的新事件对象属性
// 扩展事件属性 this.fixHooks[ type ] = fixHook = rmouseEvent.test( type ) ? this.mouseHooks : rkeyEvent.test( type ) ? this.keyHooks : {};
有一些属性是共用的,都存在,所以单独拿出来就好了
props: "altKey bubbles cancelable ctrlKey currentTarget eventPhase metaKey relatedTarget shiftKey target timeStamp view which".split(" "),
然后把私有的与公共的拼接一下
copy = fixHook.props ? this.props.concat( fixHook.props ) : this.props;
然后混入到这个新的对象上
jQuery自己写了一个基于native event的Event对象,并且把copy数组中对应的属性从native event中复制到自己的Event对象中
while ( i-- ) {
prop = copy[ i ];
event[ prop ] = originalEvent[ prop ];
}
jQuery纠正了event.target对象
jQuery官方给的解释是,Cordova没有target对象
if ( !event.target ) {
event.target = document;
}
碰巧本人做的正是cordova项目
deviceready这个是设备准备就绪的事件,没有target
在最后jQuery还不忘放一个钩子,调用fixHook.fitler方法用以纠正一些特定的event属性
例如mouse event中的pageX,pageY,keyboard event中的which
进一步修正事件对象属性的兼容问题
fixHook.filter? fixHook.filter( event, originalEvent ) : event
fixHook就是在上一章,预处理的时候用到的,分解type存进去的,针对这个特性的单独处理
最后返回这个“全新的”Event对象
事件对象默认方法的重写
可见通过jQuery.Event构造出来的新的事件对象,就是对原生事件对象的一个加强版
重写了preventDefault,stopPropagation,stopImmediatePropagation等接口由于这些方法经常会被调用中,所以这里分析一下
取消特定事件的默认行为
preventDefault: function() { var e = this.originalEvent; this.isDefaultPrevented = returnTrue; if ( e && e.preventDefault ) { e.preventDefault(); } },
重写了preventDefault方法,但是现实上其实还是调用浏览器提供的e.preventDefault方法的,唯一的处理就是增加了一个
状态机用来记录,当前是否调用过这个方法
this.isDefaultPrevented = returnTrue
同样的stopPropagation,stopImmediatePropagation都增加了 this.isPropagationStopped与 this.isImmediatePropagationStopped,
所以最后构造出来的新对象,既有原生的属性又多了很多自定义的属性方法~~ 这样jQuery可以用来玩花样了。。。
总的来说jQuery.event.fix干的事情:
- 将原生的事件对象 event 修正为一个新的可写event 对象,并对该 event 的属性以及方法统一接口
- 该方法在内部调用了 jQuery.Event(event) 构造函数
jQuery对事件体系的修正不止是做了属性兼容,重写了事件的方法,还增加状态机,那么这样的处理有什么作用?
需要处理的的问题二:数据缓存
jQuery.cache 实现注册事件处理程序的存储,实际上绑定在 DOM元素上的事件处理程序只有一个,即 jQuery.cache[elem[expando]].handle 中存储的函数,
所以只要在elem中取出当对应的prop编号去缓存中找到相对应的的事件句柄就行
这个简单了,数据缓存本来就提供接口
handlers = ( data_priv.get( this, "events" ) || {} )[ event.type ] || [],
事件句柄拿到了,是不是立刻执行呢?当然不可以,委托还没处理呢?
需要处理的的问题三:区分事件类型,组成事件队列
事件的核心的处理来了,委托的重点
如何把回调句柄定位到当前的委托元素上面,如果有多个元素上绑定事件回调要如何处理?
做这个操作之前,根据冒泡的原理,我们是不是应该把每一个节点层次的事件给规划出来,每个层次的依赖关系?
所以jQuery引入了jQuery.event.handlers用来区分普通事件与委托事件,形成一个有队列关系的组装事件处理包{elem, handlerObjs}的队列
在最开始引入add方法中增加delegateCount用来记录是否委托数,通过传入的selector判断,此刻就能派上用场了
先判断下是否要处理委托,找到委托的句柄
根据之前的测试demo,
在元素DIV下面嵌套了P,然后P内嵌套了A
此刻就要进入关键点了
分二种情况处理
第一种自然是没有委托,直接绑定的事件
body.on('click',function(){
alert('灰')
})
因为selector不存在所以delegateCount === 0,
所以委托处理的判断不成立
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) {
此时直接组装下返回elem与对应的handlers方法了
return handlerQueue.push({ elem: this, handlers: handlers.slice( delegateCount ) });
第二种就是委托处理
我们取出当然绑定事件节点上的handlers,这个是在预分析的时候做的匹配关系,具体请看上一章
得到的处理关系
从图我们可以得出
1 元素本身有事件
2 元素又要处理委托事件
那么事件的执行就需要有个先后,jQuery要如何排序呢?
依赖委托节点在DOM树的深度安排优先级,委托的DOM节点层次越深,其执行优先级越高
委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前面,委托层次越深,该事件处理程序则越靠前。
源码的处理
if ( delegateCount && cur.nodeType && (!event.button || event.type !== "click") ) { for ( ; cur !== this; cur = cur.parentNode || this ) { // Don't process clicks on disabled elements (#6911, #8165, #11382, #11764) if ( cur.disabled !== true || event.type !== "click" ) { matches = []; for ( i = 0; i < delegateCount; i++ ) { handleObj = handlers[ i ]; // Don't conflict with Object.prototype properties (#13203) sel = handleObj.selector + " "; if ( matches[ sel ] === undefined ) { matches[ sel ] = handleObj.needsContext ? jQuery( sel, this ).index( cur ) >= 0 : jQuery.find( sel, this, null, [ cur ] ).length; } if ( matches[ sel ] ) { matches.push( handleObj ); } } if ( matches.length ) { handlerQueue.push({ elem: cur, handlers: matches }); } } } }
还有几个判断条件
如果有delegateCount,代表该事件是delegate类型的绑定
找出所有delegate的处理函数列队
火狐浏览器右键或者中键点击时,会错误地冒泡到document的click事件,并且stopPropagation也无效
if ( delegateCount && event.target.nodeType && (!event.button || event.type !== "click") ) {
在当前元素的父辈或者祖先辈有可能存在着事件绑定,根据冒泡的特性,我们的依次从当前节点往上遍历一直到绑定事件的节点,取出每个绑定事件的节点对应的事件处理器
for ( ; cur !== this; cur = cur.parentNode || this ) { //遍历节点
}
这里就有个cur === this 通过这个判断来处理是否为正确的委托的
这里要注意各问题
假如elem.on('click','p',function(){}),我们在elem上点击,那么在elem的作用范围这个事件都会被触发到,如果此刻用于的目标不在P元素,但是又满足delegateCount存在
所以在cur===this,也就是目标对象就是elem了,那么判断此点击算无效点击,但是注意事件在绑定的区域内都每次触发都是会被执行的
遍历的过程需要过滤一些节点,比如disabled 属性规定应该禁用 input 元素,被禁用的 input 元素既不可用,也不可点击
if ( cur.disabled !== true || event.type !== "click" ) {
此时开始处理委托过滤的关系了
sel = handleObj.selector + " ";
我们先确定下在当前的上下文中是否能找到这个selector元素
这里用到了sizzle选择器去处理了
jQuery.find( sel, this, null, [ cur ] ).length;
如果能找到正确,是存在当然这个事件节点下面的元素,就是说这个节点是需要委托处理的
同样的的组成一个handlerQueue
handlerQueue.push({ elem: cur, handlers: matches });
根据demo点击a元素,会冒泡到P 最后到div,属于handlerQueue就有a与p的处理器了
从这里我们可以看出delegate绑定的事件和普通绑定的事件是如何分开的。
对应一个元素,一个event.type的事件处理对象队列在缓存里只有一个。
按照冒泡的执行顺序,与元素的从内向外递归,以及handlers的排序,所以就处理了
所以就形成了事件队列的委托在前,自身事件在后的顺序,这样也跟浏览器事件执行的顺序一致了
区分delegate绑定和普通绑定的方法是:delegate绑定从队列头部推入,而普通绑定从尾部推入,通过记录delegateCount来划分,delegate绑定和普通绑定。
总的来说jQuery.event.handlers干的事情:
将有序地返回当前事件所需执行的所有事件处理程序。
这里的事件处理程序既包括直接绑定在该元素上的事件处理程序,也包括利用冒泡机制委托在该元素的事件处理程序(委托机制依赖于 selector)。
在返回这些事件处理程序时,委托的事件处理程序相对于直接绑定的事件处理程序在队列的更前面,委托层次越深,该事件处理程序则越靠前。
返回的结果是 [{elem: currentElem, handlers: handlerlist}, ...] 。
事件句柄缓存分析了
事件对象兼容分析了
委托关系分析了
在从头看看事件执行的流程
绑定
elem.addEventListener( type, eventHandle, false );
事件句柄
eventHandle = elemData.handle = function( e ) { return typeof jQuery !== core_strundefined && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply( eventHandle.elem, arguments ) : undefined; };
这里其实用jQuery.event.dispatch.call就可以了,传递只是一个事件对象,然后this指向了这个事件元素elem
直接传递:jQuery.event.dispatch.call( eventHandle.elem, e) 这样不更直接吗?
call的性能在某些浏览器下要明显比apply好,而其他浏览器中两者差别不大
dispatch事件分发器源码
dispatch: function( event ) { event=
jQuery.event.fix( event ); var i, j, ret, matched, handleObj, handlerQueue = [], args = core_slice.call( arguments ), handlers= ( data_priv.get( this, "events" ) || {} )[ event.type ] ||
[], special = jQuery.event.special[ event.type ] || {}; args[0] = event; event.delegateTarget = this; if ( special.preDispatch && special.preDispatch.call( this, event ) === false ) { return; } handlerQueue= jQuery.event.handlers.call( this
, event, handlers ); i = 0; while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem;j = 0; while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } } if ( special.postDispatch ) { special.postDispatch.call( this, event ); } return event.result; },
dispatch事件分发器
可见依次处理了上面文章所以讲的三个问题
- 事件句柄缓存读取 data_priv.get
- 事件对象兼容 jQuery.event.fix
- 区分事件类型,组成事件队列 jQuery.event.handlers
1,2与步都只做修饰性的处理,关键是handlers方法,我们从中取得了handlerQueue队列
贴一下对handlerQueue事件队列的处理方法
while ( (matched = handlerQueue[ i++ ]) && !event.isPropagationStopped() ) { event.currentTarget = matched.elem; j = 0; while ( (handleObj = matched.handlers[ j++ ]) && !event.isImmediatePropagationStopped() ) { if ( !event.namespace_re || event.namespace_re.test( handleObj.namespace ) ) { event.handleObj = handleObj; event.data = handleObj.data; ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler ) .apply( matched.elem, args ); if ( ret !== undefined ) { if ( (event.result = ret) === false ) { event.preventDefault(); event.stopPropagation(); } } } } }
这个代码就是针对handlerQueue的筛选了
1 最开始就分析的事件的执行顺序,所以handlerQueue完全是按照事件的顺序排列的,委托在前,本身的事件在后面
2 产生的事件对象其实只有一份,通过jQuery.Event构造出来的event
在遍历handlerQueue的时候修改了
事件是绑定在父节点上的,所以此时的目标节点要通过替换,还有相对应的传递的数据,与处理句柄
event.currentTarget = matched.elem;
event.handleObj = handleObj;
event.data = handleObj.data;
3 执行事件句柄
ret = ((jQuery.event.special[handleObj.origType] || {}).handle || handleObj.handler).apply(matched.elem, args);
4 如果有返回值 比如return false
系统就调用
event.preventDefault();
event.stopPropagation();
根据上面的分析我们就能很好的分析出on的执行流程了
在p1上绑定了自身事件,同事绑定了委托事件到li a p上都触发,然后都调用同一个回调处理
var p1 = $('#p1') p1.on('click',function(){ console.log('灰') }) p1.on('click','li,a,p',function(e){ console.log(e) })
处理的流程:
- 同一节点事件需要绑2次,各处理各的流程,写入数据缓存elemData
- 这里要注意个问题,同一个节点上绑定多个事件,这个是在jQuery初始化绑定阶段就优化掉的了,所以触发时只会执行一次回调指令
- 触发节点的时候,先包装兼容事件对象,然后取出对应的elemData
- 遍历绑定事件节点上的delegateCount数,分组事件
- delegate绑定从队列头部推入,而普通绑定从尾部推入,形成处理的handlerQueue
- 遍历handlerQueue队列,根据判断是否isPropagationStopped,isImmediatePropagationStopped来处理对应是否执行
- 如果reuturn false则默认调用 event.preventDefault(); event.stopPropagation();
使用jQuery处理委托的优势?
从以上的分析我们不难看,jQuery对于事件的处理是极其复杂的
那么jQuery 事件委托机制相对于浏览器默认的委托事件机制而言,有什么优势?
不难发现其优势在于委托的事件处理程序在执行时,其内部的 this 指向发出委托的元素(即满足 selector 的元素),而不是被委托的元素,
记得吧
ret = ( (jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler )
.apply( matched.elem, args );
jQuery 在内部认为该事件处理程序还是绑定在那个发出委托的元素上,因此,如果开发人员在这个事件程序中中断了事件扩散—— stopPropagation,那么后面的事件将不能执行