解密jQuery事件核心 - 委托设计(二)
第一篇 http://www.cnblogs.com/aaronjs/p/3444874.html
从上章就能得出几个信息:
- 事件信息都存储在数据缓存中
- 对于没有特殊事件特有监听方法和普通事件都用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,那么后面的事件将不能执行。
当然还要涉及自定义事件,事件模拟,trigger与事件销毁,在慢慢写吧。。
文字挺多,个人见解不免有误,欢迎大家指出~~
如果觉得还可以,就顺手推荐下吧~