jQuery-1.9.1源码分析系列(十)事件系统——主动触发事件和模拟冒泡处理

王朝学院·作者佚名  2016-08-27
窄屏简体版  字體: 小  |  中  |  大  |  超大  

发现一个小点,先前没有注意的

stopPRopagation:function() {vare =this.originalEvent;

...if( e.stopPropagation ) {

e.stopPropagation();

}

jQuery重载stopPropagation函数调用的本地事件对象的stopPropagation函数阻止冒泡。也就是说,阻止冒泡的是当前节点,而不是事件源。

说到触发事件,我们第一反应是使用$(...).click()这种方式触发click事件。这种方式毫无疑问简洁明了,如果能使用这种方式推荐使用这种方式。但是如果是自定义事件呢?比如定义一个$(document).on("chuaClick","#middle",fn);这种情况怎么触发事件?这就要用到$("#middle").trigger("chuaClick")了。

a.触发事件低级API——jQuery.event.triggertrigger函数对所有类型事件的触发提供了支持。这些事件主要分为两类:普通浏览器事件(包含带有命名空间的事件如"click.chua")、自定义事件。因为要统一处理,所以函数内部实现没有调用.click()这种方式来对普通浏览器事件做捷径处理,而是统一流程。处理过程如下

1.获取要触发的事件(传入的event可能是事件类型而不是事件对象)

event = event[ jQuery.expando ] ? event :newjQuery.Event( type,typeofevent === "object" && event );

2.修正浏览器事件(主要有修正事件源)和组合正确的事件处理参数data

if( type.indexOf(".") >= 0) {//有命名空间的事件触发; 先取出事件处理入口函数handle()使用的事件类型typenamespaces = type.split(".");

type=namespaces.shift();

namespaces.sort();

}

...//调用者可以传递jQuery.Event对象,普通对象,甚至是字符串event = event[ jQuery.expando ] ?event :newjQuery.Event( type,typeofevent === "object" &&event );

event.isTrigger=true;

event.namespace= namespaces.join(".");

event.namespace_re= event.namespace ?newRegExp( "(^|\\.)" + 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();returnfalse;

}

},focus.trigger:function() {//触发本地事件保证失焦/聚焦序列正确if(this!== document.activeElement &&this.focus ) {try{this.focus();returnfalse;

}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();returnfalse;

}

}

}

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 ) {//构建一个新的事件以区别先前绑定的事件.//新构建的事件避免阻止冒泡, 但如果模拟事件可以阻止默认操作的话,我们做同样的阻止默认操作。vare =jQuery.extend(newjQuery.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.loadnoBubble:true},

click: {//checkbox触发时保证状态正确trigger:function() {if(...) {this.click();returnfalse;}}

},

focus: {//触发本当前节点blur/focus事件 确保队列正确trigger:function() {if(this!== document.activeElement &&this.focus ) {try{this.focus();returnfalse;

}catch( e ) {//IE<9,如果我们错误的让隐藏的节点获取焦点(#1486, #12518),//让.trigger()运行处理器}

}

},

delegateType:"focusin"},

blur: {

trigger:function() {if(this=== document.activeElement &&this.blur ) {this.blur();returnfalse;

}

},

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 ) {varattaches = 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);

}elseif( 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 ) {varret,

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;

}returnret;

}

};

});

需要注意的是只有在鼠标指针穿过被选元素时,才会触发 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 ) {varelem =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 ) {deleteevent._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)$/iif( 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);

});

}returnfalse;

}//事件代理; 懒惰模式为后代input节点添加change事件处理jQuery.event.add(this, "beforeactivate._change",function( e ) {varelem =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 ) {varelem =event.target;//吞掉本地单选框和复选框的change事件,我们在上面已经出发了事件if(this!== elem || event.isSimulated || event.isTrigger || (elem.type !== "radio" && elem.type !== "checkbox") ) {returnevent.handleObj.handler.apply(this, arguments );

}

},

}

OK,到此,事件系统也告一个段落了,谢谢大家多多支持。

如果觉得本文不错,请点击右下方【推荐】!

 
 
免责声明:本文为网络用户发布,其观点仅代表作者个人观点,与本站无关,本站仅提供信息存储服务。文中陈述内容未经本站证实,其真实性、完整性、及时性本站不作任何保证或承诺,请读者仅作参考,并请自行核实相关内容。
 
© 2005- 王朝網路 版權所有 導航