jQuery-1.9.1源码分析系列(十) 事件系统——主动触发事件和模拟冒泡处理
发现一个小点,先前没有注意的
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 tree
if ( 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,到此,事件系统也告一个段落了,谢谢大家多多支持。
如果觉得本文不错,请点击右下方【推荐】!
jQuery-1.9.1源码分析系列(十) 事件系统——主动触发事件和模拟冒泡处理的更多相关文章
- jQuery-1.9.1源码分析系列完毕目录整理
jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二) ...
- jQuery源码分析系列
声明:本文为原创文章,如需转载,请注明来源并保留原文链接Aaron,谢谢! 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://git ...
- [转]jQuery源码分析系列
文章转自:jQuery源码分析系列-Aaron 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAaro ...
- jQuery源码分析系列(转载来源Aaron.)
声明:非本文原创文章,转载来源原文链接Aaron. 版本截止到2013.8.24 jQuery官方发布最新的的2.0.3为准 附上每一章的源码注释分析 :https://github.com/JsAa ...
- jQuery源码分析系列——来自Aaron
jQuery源码分析系列——来自Aaron 转载地址:http://www.cnblogs.com/aaronjs/p/3279314.html 版本截止到2013.8.24 jQuery官方发布最新 ...
- jquery2源码分析系列
学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中 ...
- MyCat源码分析系列之——结果合并
更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...
- MyCat源码分析系列之——SQL下发
更多MyCat源码分析,请戳MyCat源码分析系列 SQL下发 SQL下发指的是MyCat将解析并改造完成的SQL语句依次发送至相应的MySQL节点(datanode)的过程,该执行过程由NonBlo ...
- MyCat源码分析系列之——BufferPool与缓存机制
更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...
随机推荐
- 数组和链表--Java学习笔记(一)
版权声明: 本文由Faye_Zuo发布于http://www.cnblogs.com/zuofeiyi/, 本文可以被全部的转载或者部分使用,但请注明出处. 我是一个全职妈妈,两年前在上海一家人力资源 ...
- XML数据的解析
XML数据的解析 相比于JSON数据解析而言,XML数据解析可能会让更多的童鞋感觉到吃力,对我来说,同样认为JSON数据好像让人感觉比较友好,不过对于程序开发者来说,无非就是这两种数据解析占比较大的部 ...
- 你必须知道的指针基础-4.sizeof计算数组长度与strcpy的安全性问题
一.使用sizeof计算数组长度 1.1 sizeof的基本使用 如果在作用域内,变量以数组形式声明,则可以使用sizeof求数组大小,下面一段代码展示了如何使用sizeof: ,,,,,}; int ...
- Key/Value之王Memcached初探:三、Memcached解决Session的分布式存储场景的应用
一.高可用的Session服务器场景简介 1.1 应用服务器的无状态特性 应用层服务器(这里一般指Web服务器)处理网站应用的业务逻辑,应用的一个最显著的特点是:应用的无状态性. PS:提到无状态特性 ...
- C++高精度计时器——微秒级时间统计
在C++中,经常需要通过计时来统计性能信息,通过统计的耗时信息,来分析性能瓶颈,通常情况下,可能毫秒级别的时间统计就足够用了,但是在毫厘必争的性能热点的地方,毫秒级别的统计还是不够的,这种情况下,就需 ...
- 代码提交的时候可以插入表情了-GitHub表情的使用
GitHub官方有个表情项目,旨在丰富文字信息.意味着你可以在提交代码的时候,在提交信息里面添加表情,同时也可以在项目的ReadMe.md文件里面使用表情.除此之外,当然还有项目在GitHub上的wi ...
- 《Entity Framework 6 Recipes》翻译系列 (5) -----第二章 实体数据建模基础之有载荷和无载荷的多对多关系建模
2-3 无载荷(with NO Payload)的多对多关系建模 问题 在数据库中,存在通过一张链接表来关联两张表的情况.链接表仅包含连接两张表形成多对多关系的外键,你需要把这两张多对多关系的表导入到 ...
- Atitit 深入理解耦合Coupling的原理与attilax总结
Atitit 深入理解耦合Coupling的原理与attilax总结 耦合是指两个或两个以上的电路元件或电网络等的输入与输出之间存在紧密配合与相互影响,并通过相互作用从一侧向另一侧传输能量的现 ...
- 将数据从MySQL迁移到Oracle的注意事项
将数据从MySQL迁移到Oracle的注意事项1.自动增长的数据类型处理MYSQL有自动增长的数据类型,插入记录时不用操作此字段,会自动获得数据值.ORACLE没有自动增长的数据类型,需要建立一个自动 ...
- C#设计模式系列:策略模式(Strategy)
1.策略模式简介 1.1>.定义 策略是为达到某一目的而采取的手段或方法,策略模式的本质是目标与手段的分离,手段不同而最终达成的目标一致.客户只关心目标而不在意具体的实现方法,实现方法要根据具体 ...