发现一个小点,先前没有注意的
stoppropagation: function() { var e = this.originalevent; ... if ( e.stoppropagation ) { e.stoppropagation(); }
jquery重载stoppropagation函数调用的本地事件对象的stoppropagation函数阻止冒泡。也就是说,阻止冒泡的是当前节点,而不是事件源。
说到触发事件,我们第一反应是使用$(...).click()这种方式触发click事件。这种方式毫无疑问简洁明了,如果能使用这种方式推荐使用这种方式。但是如果是自定义事件呢?比如定义一个$(document).on(chuaclick,#middle,fn);这种情况怎么触发事件?这就要用到$(#middle).trigger(chuaclick)了。
a.触发事件低级api——jquery.event.trigger
trigger函数对所有类型事件的触发提供了支持。这些事件主要分为两类:普通浏览器事件(包含带有命名空间的事件如click.chua)、自定义事件。因为要统一处理,所以函数内部实现没有调用.click()这种方式来对普通浏览器事件做捷径处理,而是统一流程。处理过程如下
1.获取要触发的事件(传入的event可能是事件类型而不是事件对象)
event = event[ jquery.expando ] ? event :new jquery.event( type, typeof event === object && event );
2.修正浏览器事件(主要有修正事件源)和组合正确的事件处理参数data
if ( type.indexof(.) >= 0 ) { //有命名空间的事件触发; 先取出事件处理入口函数handle()使用的事件类型type namespaces = type.split(.); type = namespaces.shift(); namespaces.sort(); } ...// 调用者可以传递jquery.event对象,普通对象,甚至是字符串 event = event[ jquery.expando ] ? event : new jquery.event( type, typeof event === object && event ); event.istrigger = true; event.namespace = namespaces.join(.); event.namespace_re = event.namespace ? new regexp( (^|\\.) + namespaces.join(\\.(?:.*\\.|)) + (\\.|$) ) : null; // 重置result属性,避免上次的结果残留 event.result = undefined; if ( !event.target ) { event.target = elem; } // 克隆传参data并将event放在传参data的前面,创建出事件处理入口函数的参数列表,创建后结果可能是[event,data] data = data == null ? [ event ] : jquery.makearray( data, [ event ] );
后面这段组合事件处理参数列表data在后面处理时调用
if ( handle ) { handle.apply( cur, data ); }
3.判断是否是特殊节点对象的的特殊事件,是的话特殊处理
special = jquery.event.special[ type ] || {};if ( !onlyhandlers && special.trigger && special.trigger.apply( elem, data ) === false ) {return;}
这里面需要特殊处理的事件比较少,这里列一下
special: { click.trigger: function(){ // checkbox, 触发本地事件确保状态正确if ( jquery.nodename( this, input ) && this.type === checkbox && this.click ) { this.click(); return false; } }, focus.trigger: function() { // 触发本地事件保证失焦/聚焦序列正确if ( this !== document.activeelement && this.focus ) { try { this.focus(); return false; } catch ( e ) { // support: ie<9 // if we error on focus to hidden element (#1486, #12518), // let .trigger() run the handlers } } }, blur.trigger: function() {if ( this === document.activeelement && this.blur ) { this.blur(); return false; } }}
4.从事件源开始遍历父节点直到window对象,将经过的节点保存(保存到eventpath)下来备用
for ( ; cur; cur = cur.parentnode ) {eventpath.push( cur );tmp = cur;}// 将window也压入eventpath(e.g., 不是普通对象也不是断开连接的dom)if ( tmp === (elem.ownerdocument || document) ) {eventpath.push( tmp.defaultview || tmp.parentwindow || window );}
5.循环先前保存的节点,访问节点缓存,如果有对应的事件类型处理队列则取出其绑定的事件(入口函数)进行调用。
// jquery绑定函数处理:判断节点缓存中是否保存相应的事件处理函数,如果有则执行 handle = ( jquery._data( cur, events ) || {} )[ event.type ] && jquery._data( cur, handle ); if ( handle ) { handle.apply( cur, data ); } // 本地绑定处理 handle = ontype && cur[ ontype ]; if ( handle && jquery.acceptdata( cur ) && handle.apply && handle.apply( cur, data ) === false ) { event.preventdefault(); }
6. 最后处理浏览器默认事件,比如submit标签的提交表单处理。
// 如果没有人阻止默认的处理,执行之 if ( !onlyhandlers && !event.isdefaultprevented() ) { ... }
注意:普通事件加上命名空间仍然属于普通事件,普通调用方式依然其作用。比如$(document).on('click.chua',#id,fn1).on(click,#id,fn2);当点击“#id”节点的时候fn1依然会被调用。触发指定命名空间事件的唯一方式是trigger:$(#id).trigger(click.chua),此时只会调用fn1。
从第4、5个步骤可以看到trigger的另外一个巨大作用——模拟冒泡处理。后面会分析到
b. 事件特殊处理jquery.event.special(主要有事件替代、模拟冒泡)详解
委托设计是基于事件可冒泡的。但是有些事件是不可冒泡的,有的事件在不同的浏览器上支持的冒泡情况不同。还有不同的浏览器支持的事件类型也不尽相同。这些处理主要都被放在jquery.event.special中。jquery.event.special对象中保存着为适配特定事件所需的变量和方法。
具体有:
delegatetype / bindtype (用于事件类型的调整)
setup (在某一种事件第一次绑定时调用)
add (在事件绑定时调用)
remove (在解除事件绑定时调用)
teardown (在所有事件绑定都被解除时调用)
trigger (在内部trigger事件的时候调用)
nobubble
_default
handle (在实际触发事件时调用)
predispatch (在实际触发事件前调用)
postdispatch (在实际触发事件后调用)
看一下模拟冒泡的函数simulate
simulate: function( type, elem, event, bubble ) { // 构建一个新的事件以区别先前绑定的事件. // 新构建的事件避免阻止冒泡, 但如果模拟事件可以阻止默认操作的话,我们做同样的阻止默认操作。 var e = jquery.extend( new jquery.event(), event, { type: type, issimulated: true, originalevent: {} } ); if ( bubble ) { jquery.event.trigger( e, null, elem ); } else { jquery.event.dispatch.call( elem, e ); } if ( e.isdefaultprevented() ) { event.preventdefault(); } }
看到没有,真正模拟冒泡函数是jquery.event.trigger函数
special第一组
这里面涉及到冒泡处理的问题。
special: { load: { //阻止触发image.load事件冒泡到window.load nobubble: true }, click: { //checkbox触发时保证状态正确 trigger: function() {if (...) {this.click();return false;}} }, focus: { //触发本当前节点blur/focus事件 确保队列正确 trigger: function() { if ( this !== document.activeelement && this.focus ) { try { this.focus(); return false; } catch ( e ) { // ie<9,如果我们错误的让隐藏的节点获取焦点(#1486, #12518), // 让.trigger()运行处理器 } } }, delegatetype: focusin }, blur: { trigger: function() { if ( this === document.activeelement && this.blur ) { this.blur(); return false; } }, delegatetype: focusout }, beforeunload: { postdispatch: function( event ) { //即使的returnvalue等于undefined,firefox仍然会显示警告 if ( event.result !== undefined ) { event.originalevent.returnvalue = event.result; } } }}
focus/blur本来是不冒泡的,但是我们依然可以通过$(document).on('focus ','#left',fn)来绑定,是怎么做到的?我们来看jquery的处理
第一步,将focus绑定的事件转化为focusin来绑定,focusin在w3c的标准中是冒泡的,除开火狐之外的浏览器也确实支持冒泡(火狐浏览器focusin/focusout支持冒泡的兼容后面会详解)
type = ( selector ? special.delegatetype : special.bindtype ) || type;
然后,根据新得到的type类型(focusin)获取新的special
special = jquery.event.special[ type ] || {};
获取的special结果为
jquery.each({ focus: focusin, blur: focusout }, function( orig, fix ) {var attaches = 0,handler = function( event ) {//模拟冒泡jquery.event.simulate( fix, event.target, jquery.event.fix( event ), true );};jquery.event.special[ fix ] = { setup: function() { if ( attaches++ === 0 ) { document.addeventlistener( orig, handler, true ); } }, teardown: function() { if ( --attaches === 0 ) { document.removeeventlistener( orig, handler, true ); } }};});
再然后,就是绑定事件,绑定事件实际上就对focusin、focusout做了兼容处理,源码中第一个判断有special.setup.call(…)这段代码,根据上面setup函数可见第一次进入的时候实际上是通过setup函数中的document.addeventlistener( orig, handler, true )绑定事件,注意:第一个参数是是orig,因为火狐不支持focusin/focusout所以jquery使用focus/blur替代来监听事件;注意第三个参数是true,表示在事件捕获阶段触发事件。
我们知道任何浏览器捕获都是从外层到精确的节点的,所有的focusin事件都会被捕获到,然后执行handler函数(里面是jquery.event.simulate函数,源码略)。其他事件绑定则进入if分支将事件直接绑定到elem上
if ( !special.setup || special.setup.call( elem, data, namespaces, eventhandle ) === false ) {if ( elem.addeventlistener ) {elem.addeventlistener( type, eventhandle, false ); } else if ( elem.attachevent ) {elem.attachevent( on + type, eventhandle );}}
special第二组:mouseenter/mouseleave
//使用mouseover/out和事件时机检测创建mouseenter/leave事件jquery.each({ mouseenter: mouseover, mouseleave: mouseout }, function( orig, fix ) { jquery.event.special[ orig ] = { delegatetype: fix, bindtype: fix, handle: function( event ) { var ret, target = this, related = event.relatedtarget, handleobj = event.handleobj; //对于mousenter/leave,当related在target外面的时候才调用handler //参考: 当鼠标离开/进入浏览器窗口的时候是没有relatedtarget的 if ( !related || (related !== target && !jquery.contains( target, related )) ) { event.type = handleobj.origtype; ret = handleobj.handler.apply( this, arguments ); event.type = fix; } return ret; } };});
需要注意的是只有在鼠标指针穿过被选元素时,才会触发 mouseenter 事件。对应mouseleave这样的话,mouseenter子元素不会反复触发事件,否则在ie中经常有闪烁情况发生
使用mouseover/out和事件时机检测创建mouseenter/leave事件有个关键的判断
if ( !related || (related !== target && !jquery.contains( target, related )) )
其中!jquery.contains( target, related )表示related在target外面。我们使用图例来解释
我们假设处理的是mouseenter事件,进入target。
鼠标从related到target,很明显related在target外面,所以当鼠标移动到target的时候满足条件,调用处理。
现在反过来,很明显related在target里面,那么鼠标之前就处于mouseenter状态(意味着之前就进行了mouseenter处理器处理),避免重复调用当然是不进行任何处理直接返回了。
我们假设处理的是mouseleave事件,离开target。
鼠标从target到related,很明显related在target里面,所以当鼠标移动到related的时候依然么有离开target,不做处理。
鼠标从target到related,很明显related在target外面,所以当鼠标移动到related的时候已经离开了target的范围,做处理。
special第三组:submit和change
主要是ie下submit不能冒泡的处理
jquery.event.special.submit主要有一下几个特征
setup
postdispatch
teardown
根据添加事件的代码可知添加事件的时候如果符合条件则会调用setup来添加事件
if ( !special.setup || special.setup.call( elem, data, namespaces, eventhandle ) === false )
jquery在ie下模拟submit事件以click和keypress替代,只不过是添加了命名空间来区别和普通click和keypress事件。
setup: function() {...jquery.event.add( this, click._submit keypress._submit, function( e ) {var elem = e.target,form = jquery.nodename( elem, input ) || jquery.nodename( elem, button ) ? elem.form : undefined;if ( form && !jquery._data( form, submitbubbles ) ) {jquery.event.add( form, submit._submit, function( event ) {event._submit_bubble = true;});jquery._data( form, submitbubbles, true );}});},
在事件调用过程中(dispatch)会调用postdispatch来处理
if ( special.postdispatch ) { special.postdispatch.call( this, event );}postdispatch中调用simulate完成事件处理postdispatch: function( event ) {// if form was submitted by the user, bubble the event up the treeif ( event._submit_bubble ) {delete event._submit_bubble;if ( this.parentnode && !event.istrigger ) {jquery.event.simulate( submit, this.parentnode, event, true );}}},
teardown用在删除事件绑定中
ie下change事件的处理和submit类似,事件使用beforeactivate替代来监听,处理函数变成了handle,在事件分发(dispatch)中执行代码
ret = ( (jquery.event.special[ handleobj.origtype ] || {}).handle || handleobj.handler ).apply( matched.elem, args );
主要源码如下
jquery.event.special.change = { setup: function() { //rformelems = /^(?:input|select|textarea)$/i if ( rformelems.test( this.nodename ) ) { // ie不会在check/radio失焦前触发change事件; 在属性更改后触发它的click事件 // 在special.change.handle中会吞掉失焦触发的change事件. // 这里任然会在check/radio失焦后触发onchange事件. if ( this.type === checkbox || this.type === radio ) { jquery.event.add( this, propertychange._change, function( event ) { if ( event.originalevent.propertyname === checked ) { this._just_changed = true; } }); jquery.event.add( this, click._change, function( event ) { if ( this._just_changed && !event.istrigger ) { this._just_changed = false; } // allow triggered, simulated change events (#11500) jquery.event.simulate( change, this, event, true ); }); } return false; } // 事件代理; 懒惰模式为后代input节点添加change事件处理 jquery.event.add( this, beforeactivate._change, function( e ) { var elem = e.target; if ( rformelems.test( elem.nodename ) && !jquery._data( elem, changebubbles ) ) { jquery.event.add( elem, change._change, function( event ) { if ( this.parentnode && !event.issimulated && !event.istrigger ) { jquery.event.simulate( change, this.parentnode, event, true ); } }); jquery._data( elem, changebubbles, true ); } }); }, handle: function( event ) { var elem = event.target; // 吞掉本地单选框和复选框的change事件,我们在上面已经出发了事件 if ( this !== elem || event.issimulated || event.istrigger || (elem.type !== radio && elem.type !== checkbox) ) { return event.handleobj.handler.apply( this, arguments ); } },}
ok,到此,事件系统也告一个段落了,谢谢大家多多支持。
