第十一章:事件系统
事件系统是一个框架非常重要的部分,用于响应用户的各种行为。
浏览器提供了3个层次的api,用于响应用户的各种行为。
1.最原始的是写在元素标签内。
2.再次是脚本内,以el.onXXX = function绑定的方式,统称为DOM0事件系统。
3.最后是多投事件系统,一个元素的同一类型事件可以绑定多个回调,统常称为DOM2事件系统。
由于浏览器大战,现存两套API。
IE与opera:
绑定事件:el.attachEvent("on"+ type, callback)
卸载事件:el.detachEvent("on"+ type. callback)
创建事件:el.document.createEventObject()
派发事件:el.fireEvent(type,event)
w3c:
绑定事件:el.addEventListener(type,callback,[phase])
卸载事件:el.removeEventListener(type,callback,[phase])
创建事件:el.createEvent(types)
初始化事件:event.initEvent()
派发事件:el.dispatchEvent(event)
从api的数量与形式来看,w3c提供的复杂很多,相对于也强大很多,下面我们将逐一分析
首先我们先来几个简单的例子,没必要动用框架。不过事实上,整个事件系统就建立在它们的基础上。
function addEvent (el, callback, useCapture) { if(el.dispatchEvent){//w3c优先 el.addEventListener(type, callback, !!useCapture ); } else { el.attachEvent( "on"+ type, callback ); } return callback; //返回callback方便卸载时用 } function removeEvent (el, type, callback, useCapture) { if (el.dispatchEvent) { //w3c优先 el.removeEventListener (type, callback, !!useCapture); } else { el.detachEvent( "on"+type, callback ) } } function fireEvent (el, type, args, event) { args = args || {} if (el.dispatchEvent) { //w3c优先 event = document.createEvent("HTMLEvents"); event.initEvent(type, true, true); } else { event = document.createEventObject(); } for (var i in args) if (args.hasOwnProperty(i)) { event[i] = args[i] } if (el.dispatchEvent) { el.dispatchEvent(event); } else { el.fireEvent('on'+type , event) } }
一,onXXX绑定方式的缺陷
onXXX既可以写在html标签内,也可以独立出来,作为元素节点的一个特殊属性来处理,不过作为一个古老的绑定方式,它很难预料到人们对这方面的扩展。
总结下来有以下不足:
1.onXXX对DOM3新增的事件或FF某些私有实现无法支持,主要有以下事件:
DOMActivate
DOMAttrModified
DOMAttributeNameChanged
DOMCharacterDataModified
DOMContentLoaded
DOMElementNameChanged
DOMFocusIn
DOMFocusOut
DOMMouseScroll
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDcouemnt
DOMSubtreeModified
MozMousePixelScroll
2.onXXX只允许元素每次绑定一个回调,重复绑定冲掉之前的绑定
3.onXXX在IE下回调没有参数,在其他浏览器回调的第一个参数是事件对象。
4.onXXX只能在冒泡阶段可用。
二,attachEvent的缺陷
attachEvent是微软在IE5添加的API,Opera也支持,也对于onXXX方式,它可以允许同一种元素同一种事件绑定多个回调,也就是所谓多投事件机制。但带来的麻烦只多不少,存在以下几点缺陷。
1.ie下只支持微软的事件系统,DOM3事件一概不支持。
2.IE下attchEvent回调中的this不是指向被绑定元素,而是window!
3.IE下同种事件绑定多个回调时,回调并不是按照绑定时的顺序依次触发的!
4.IE下event事件对象与w3c的存在太多差异了,有的无法对上号,比如currentTarget
5.IE还是只支持冒泡阶段。
关于事件对象,w3c是大势所趋,在IE9支持W3c那一套API时,这对我们实现事件代理非常有帮助。
三,addEventListener的缺陷
w3c这一套API也不是至善至美,毕竟标准总是滞后于现实,剩下的标准浏览器各有自己的算盘,它们之间也有不一致的地方。
1.新事件非常不稳定,可能还有普及就开始被废弃,在早期的sizzle选择器引擎中,有这么几句。
document.addEventListener("DOMAttrModified", invalidate, false); document.addEventListener("DOMNodeInserted", invalidate, false); document.addEventListener("DOMNodeRemoved", invalidate, false);
现在这三个事件被废弃了(准确的说,所有变动事件都完蛋了),FF14和chrome18开始使用MutationObserver代替它。
2.Firefox不支持focusin,focus事件,也不支持DOMFocusIn,DOMFocusOut,现在也不愿意用mouseWheel代替DOMMouseScroll。chrome不支持mouseenter与mouseleave.
因此,不要以为标准浏览器就肯定实现了w3c标准事件,所有特征侦测必不可少。
3.第三个,第四个,第五个标准参数。
第三个参数,useCapture只有非常新的浏览器才是可选项。比如FF6或之前是可选的,为了安全起见,确保第三个参数为布尔。
4.事件成员的不稳定。
w3c是从浏览器商抄过来的,人家用了这么久,难免与标准不一致。
ff下event.timeStamp返回0的问题,这个bug,2004年就有人提交了,直到2011年才被修复。
Safari下event.target可能返回文本节点
event.defaultPrevented,event.isTrusted与stopImmediatePropagation方法,之前标准浏览器都统一用getpreventDefault方法做这个事情,在jQuery源码中,发现它是用isDefaultPrevented来处理。
isTrusted属性用于表示当前事件是否是由用户行为触发,比如是用一个真实的鼠标点击触发click事件,还是由一个脚本生成的(使用事件构造方法,比如event.initEvent)。isTrusted请多关注
5.标准浏览器没有办法模拟像IE6-8的proprtychange事件。
虽然标准的浏览器有input, DOMAttrModified,MutationObserver,但比起propertychange弱爆了。propertychange可以监听多种属性变化,而不单单是value值。另外它不区分attribute和property。因此,无论是通过el.xxx = yyy 还是el.setAttribute(xxx,yyy)都接触此事件。
http://www.cnblogs.com/rubylouvre/archive/2012/05/26/2519263.html (判断浏览器是否支持DOMAttrModified)
四,Dean Edward的addEvent.js源码分析
这是一个prototype时代早期出现的一个事件系统。jQuery事件系统源头。亮点如下:
1.有意识的屏蔽IE与w3c在阻止默认行为与事件传播接口的差异。
2.处理ie执行回调时的顺序问题
3.处理ie的this指向问题
4.没有平台检测代码,因为是使用最通用最原始的onXXX构建
5.完全跨浏览器(IE4与NS4)。
此处省略源码分析
http://dean.edwards.name/weblog/2005/10/add-event/
// written by Dean Edwards, 2005 // with input from Tino Zijdel, Matthias Miller, Diego Perini // http://dean.edwards.name/weblog/2005/10/add-event/ function addEvent(element, type, handler) { if (element.addEventListener) { element.addEventListener(type, handler, false); } else { // assign each event handler a unique ID if (!handler.$$guid) handler.$$guid = addEvent.guid++; // create a hash table of event types for the element if (!element.events) element.events = {}; // create a hash table of event handlers for each element/event pair var handlers = element.events[type]; if (!handlers) { handlers = element.events[type] = {}; // store the existing event handler (if there is one) if (element["on" + type]) { handlers[0] = element["on" + type]; } } // store the event handler in the hash table handlers[handler.$$guid] = handler; // assign a global event handler to do all the work element["on" + type] = handleEvent; } }; // a counter used to create unique IDs addEvent.guid = 1; function removeEvent(element, type, handler) { if (element.removeEventListener) { element.removeEventListener(type, handler, false); } else { // delete the event handler from the hash table if (element.events && element.events[type]) { delete element.events[type][handler.$$guid]; } } }; function handleEvent(event) { var returnValue = true; // grab the event object (IE uses a global event object) event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event); // get a reference to the hash table of event handlers var handlers = this.events[event.type]; // execute each event handler for (var i in handlers) { this.$$handleEvent = handlers[i]; if (this.$$handleEvent(event) === false) { returnValue = false; } } return returnValue; }; function fixEvent(event) { // add W3C standard event methods event.preventDefault = fixEvent.preventDefault; event.stopPropagation = fixEvent.stopPropagation; return event; }; fixEvent.preventDefault = function() { this.returnValue = false; }; fixEvent.stopPropagation = function() { this.cancelBubble = true; };
不过在Dean Edward对应的博文中就可以看到许多指正与有用的patch。比如说,既然所有的修正都是冲着IE去的,那么标准浏览器用addEventListener就行。有的还提到,在iframe中点击事件时,事件对象不对的问题,提交以下有用的补丁。
event = event || fixEvent(((this.ownerDocument || this.document || this).parentWindow || window).event);
其中,第54条回复,直接导致了jQuery数据缓存系统的产生,为了避免交错引用产出的内存泄露,建议元素分配一个uuid,所有的回调都放在一个对象中存储。
但随着事件的推移,使用者发现onXXX在IE存在不可消除和弥补的内存泄露,因此,翻看jQuery早期的版本,1.01是照抄Dean Edward的,1.1.31版本,开始吸收54条uuid的建议,并使用attach/removeEventListener绑定事件——每个元素只绑定一次。然后所有回调都在类似handleEvent的函数中调用。
五,jQuery1.8.2的事件模块概述
jQuery的事件模块发端于Dean Edward的addEvent,然后它不断吸收社区的插件与补丁,发展成为非常棒的事件系统。其中不得不提的是事件代理与事件派发机制。
早在07年,Brandon Aaron为jQuery写了一个划时代的插件,叫livequery,它可以监听后来插入的元素的事件。比如说,一个表格,我们为tr元素绑定了mouseover/mouseout事件时,只有十行代码,然后我们又动态加载了20行,这20个tr元素同样能执行mouseover/mouseout回调。魔术在于,它并没有把事件侦探器绑定在tr元素上,而是绑定在最顶层的document上,然后通过事件冒泡,取得事件源,判定它是否匹配给用户给定的css表达式,才执行用户回调、具体参考它的github:
https://github.com/brandonaaron/livequery
如果一个表格有100个 tr元素,每个都要绑定mouseover/mouseout事件,改成事件代理的方式,可以节省99次绑定,这优化很好,更何况它能监听将来添加的元素,因此被立马吸收到jquery1.3中去,成为它的live方法,再把一些明显的bug修复了。jquery1.32成为最受欢迎的版本。与后来的jquery1.42都是历程碑式的。
不过话说回来,live方法需要对某些不冒泡的事件做些处理,比如一些表单事件,有的只能冒泡的form,有的只能冒泡到document,有的根本就不冒泡。
ie6 | ie8 | ff3.6 | opera10 | chome4 | sfari4 | |
submit | form | form | document | document | document | document |
reset | form | form | document | document | form | form |
change | 不冒泡 | 不冒泡 | document | document | 不冒泡 | 不冒泡 |
click | document | document | document | document | document | document |
select | 不冒泡 | 不冒泡 | document | document | 不冒泡 | 不冒泡 |
对于focus, blur, change, submit, reset, select等不会冒泡的事件,在标准浏览器中,我们我们可以设置addEventListener最后的一个参数为true轻松搞定,在IE就有点麻烦了,要用focusin代替focus,focusout代替blur,selectsatrt代替select,change和submit,reset就更复杂了。必须使用其他的事件来模拟,还要判断事件源的类型,slelectedIndex, keyCode等相关属性。
这个课题到最后由一个叫reglib的库搞定,reglib的作者还写过一篇很著名的文章,《goodbuy mouseover,hello mouseenter》,来推广微软系统的两个事件,mouseenter与mouseleave。jQuery全面接纳了他们。
live方法带来的全新体验是空前的,但毕竟要冒泡到最顶层,对IE来说有点坎坷,还会失灵。最好能指定父节点,一个绑定时已经存在的父节点。这样就不用费力了。当时有三篇博文给出了相近的方案,他们给出的接口一篇比一篇接近jhhon Resig接纳的方案。
http://danwebb.net/2008/2/8/event-delegation-made-easy-in-jquery
http://raxanpdi.com/blog-jquery-event-delegate.html (jQuery Event Delegation)
http://blog.threedubmedia.com/2008/10/jquerydelegate.html(已经打不开)
那时,它已经是这个样子
$('div').delegate('click','span',function (event) { $(this).toggleClass('selected'); return false; })
并提出解除代理的API:undelegate
在jquery1.42在2010年2月19推出时,也这两个接口,前面的两个参数只是掉换一下。
$('div').delegate('span','click',function( event ){ $( this ).toggleClass('selected'); return false; })
正所众人拾柴火焰高,jquery强大无不道理,在jquery1.8中,它又吸收dperini/nwevents的点子,改进其代理事件,大大 提高了性能。
先看一下其主要接口:
Jquery事件接口:bind live delegate on one trigger hover unbind die undelegate off toggle triggerHander Event .
其中bind,unbind,one,trigger,toggle,hover,ready一开始就有。
triggerHandler是jQuery1.23增加的,内部依赖于trigger,只对当前匹配元素的第一个有效,不冒泡不触发默认行为。
live与die是jQuery1.3增加的,用于事件代理,统一由document代理
delegate与undelegate是jquery1.4增加的,允许指定代理元素的事件代理,它内部是利用live,die实现的。
on与off是jQuery1.7增加的,目的是统一事件接口,bind,one,live,delegate,直接由on衍生,unbind,die,dundelegate直接由off衍生。
hover用于模拟css的hover效果,内部依赖于mouseenter,mouseleave
ready可以看做是load事件的增强版,获取最早的DOM可用后,立即执行各种dom操作。
toggle是click的增强版,每次点击都执行不同的回调,并切换到下一个。
triggle与triggleHander是jQuery的fireEvent实现
此外,jQuery还有25个事件类型命名的快捷方法,当参数个数为2时表现为绑定事件。个数为0时表现为派发事件。
jQuery快捷事件:contextmenu, error, keyup, keypress, keydown, submit, select, change, mouseleave, mouseenter, mouseout, mouseover, blur, focus, focusin, focusout, load, resize, scroll, unload, click, dbclik,mousedown, mouseup, mousemove
不过,最重要最基础的设施无疑是jQuery.event之下
add方法用于绑定事件。
remove方法用于卸载事件。
dispath方法用于统一用户回调,相当于Dean Edward的handleEvent方法,因此,jQuery1.7前它的名字一直叫handle。
trigger用于事件派发。
fix用于修正事件对象。
自jQuery1.4起,jQuery大大强化自定义事件功能。special对象本来是用于修正个别事件的,现在它允许这些事件通过setup, teardown, add, remove, default这些接口实现DOM事件的各种行为。
参考:
http://benalman.com/news/2010/03/jquery-special-events/
六,jQuery.event.add源码解读
add方法的主要目的,将用户所有的传递参数,合并成一个handleObj对象放到元素对应的缓存体中events对象的某个队列中,然后绑定一个回调。这个回调会处理用户的所有回调。因此,对于每个元素的每一种事件,它只绑定一次。
var add = function(elem, types, handler, data, selector) { var elemData, eventHandle, events, t, tns, namespaces, handleObj; //如果elem不能添加自定义属性,由于ie下访问文本节点会报错,因此事件源不能是文本节点。 //注释节点本来就不应该绑定事件,注释节点之所以混进来,是因为jQuery的html方法所致 //如果没有指定事件的类型或回调也立即返回,不再向下操作 if (elem.nodeType === 3 || elem.nodeType === 8 || !types || !handler || !(elemData = jQuery._data(elem))) { return; } //取得用户回调与css表达式,handleObjIn这种结构我们称为事件描述 //记下用户绑定此回调的各种信息,方便用于“事件拷贝” if (handler.handler) { handlerObjIn = handler; handler = handlerObjIn.handler; selector = handlerObjIn.selector; } //确保回调有uuid,用于查找和移除 if (!handler.guid) { handler.guid = jQuery.guid++; } //为元素在数据缓存系统中开辟一个叫"event"的空间来保存其所有回调与事件处理器 events = elemData.events; if (!events) { elemData.events = events = {}; } eventsHandle = elemData.handle; //事件处理器 if(eventHandle) { elemData.handle = eventHandle = function(e) { //用户在事件冒充时,被第二次fire或者页面的unload后触发 return typeof jQuery !== "undefined" && (!e || jQuery.event.triggered !== e.type) ? jQuery.event.dispatch.apply(eventsHandle.elem, arguments) : undefined; }; //原注释是说,防止IE下非原生的事件内存泄露,(直接影响是明确了this的指向) eventHandle.elem = elem; } //通过空格隔开同时绑定多个事件,比如jQuery(...).bind("mouseover mouseout", fn); types = jQuery.trim(hoverHack(types)).split(" "); for (t = 0 ; t < types.length; t++) { tns = rtypenamespace.exec(types[t]) || [] ; //取得命名空间 type = tnsp[1];//取得真正的事件 namespaces = (tns[2] || "").split(".").sort();//修正命名空间 //并不是所有事件都能直接使用,比如FF下没有mousewheel,需要用DOMMouseScroll冒充 special = jQuery.event.special[ type ] || {}; //有时候,我们只需要在事件代理时进行冒充,比如FF下的focus, blur type = (selector ? special.delegateType : special.bindType) || type; special = jQuery.event.special[type] || {}; //构建一个事件描述对象 handleObj = jQuery.extend({ type : type, origType : tns[1], data : data, handler : handler, guid : handler.guid, selector : selector, needsContext : selector && jQuery.expr.match.needsContext.test(selector), namespace : namespaces.join(".") },handlerObjIn); //在events对象上分门别类的存储事件描述,每种事件对于一个数组 //每种事只绑定一次监听器(既addEventListener, attachEvent) handlers = events [type]; if (!handlers) { handlers = events[ type ] = []; handlers.delegateCount = 0; //记录要处理的回调个数 //如果存在special.setup并且special.setup返回0个才直接使用多投事件API 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); } } } if (special.add) { //处理自定义事件 special.add.call(elem, handleObj); if (!handleObj.handler.guid) { handleObj.handler.guid = handler.guid; } } //add to the element's hanlder list, deleate in front if (selector) { //如果是事件代理,那么把此事件代理描述放在数据的前面 handlers.splice(handlers.delegateCount++, 0, handleObj); } else { handlers.push(handleObj); } //用于jQuery.event.trigger,如果此事件从未绑定过,也没必要进入trigger的真正处理逻辑 jQuery.event.golbal[ type ] = true; } //防止ie内存泄露 elem = null }
从上面的注释中我们可以得出,jQuery的回调不再直接与元素挂钩,而是通过uuid访问数据缓存系统,抵达对于的events对象,再根据事件类型得到一组事件描述。并且事件描述里边没有事件源的记录,因此,非常方便导出挪动,为事件克隆大开方便之门。当然,其中数据缓存系统是关键,接着探索,就会发现,其它事件代理部分对数据缓存依赖的更严重。
下面是元素数缓存与事件描述之间的结构:
在firebug下查看结构:如下
七,jQuery.event.remove的源码解读
remove方法的主要目的是,根据用户传参,找到事件队列,从里边把匹配的handleObj对象移除,在参数不足的情况,可能移除N个或所有。当队列的长度为零就移除事件,当events为空对象,则清除掉UUID。
remove = function(elem, types, handler, selector) { var t, tns, type, origType,namespaces, origCount, j, events, special, eventType, handleObj, elemData = jQuery.hasData( elem ) && jQuery._data( elem ); //如果不支持添加自定义属性或没有缓存与事件有关的东西,立即返回。 if ( !elemData || !(events = elemData.events)) { return; } //hover转换为"mouseenter mouseleave",并且按照空格进行切割,方便移除多种事件类型 types = jQuery.trim( hoverHack( types || "")).split(" "); for (t = 0; t < types.length; t++) { tns = rtypenamespace.exec(types[t]) || []; type = origType = tns[1]; //取得事件类型 namespaces = tns[2];//取得命名空间 if (!type) { //如果没有指定事件类型,则移除所有事件类型或移除所有与此命名空间相关的事件类型 for (type in events) { jQuery.event.remove( elem, type + types[t], handler, selector, true); } continue; } //利用事件冒充,取得真正的用于绑定事件类型 special = jQuery.event.special[ type ] || {}; type = (selector ? special.delegateType : special.bindType) || type; eventType = events[type] || []; //取得装载事件描绘对象的数组 origCount = eventType.length; //取得用于过滤命名空间的正则,没有为null namespaces = namespaces ? new RegExp("(^|\\.)" + namespaces.split(".").sort().join("\\.(?:.*\\.|)") + "(\\.|$)" : null ; //移除符合条件的事件描述 for ( j = 0; j < eventType.length; j++) { handleObj = eventType[ j]; if (( origType === handleObj.guid ) & //比较事件类型是否一致 (!handler || handler.guid === handleObj.guid) && //如果传递了回调,判定UUid是否相同 //如果types含义命名空间,用正则看是否匹配 //如果事件代理必有css表达式,比较与事件描述对象中是否相等 (!selector || selector === handleObj.selector || selector === "**" && handleObj.selector)) { eventType.splice(j-- , 1); //是就移除 if ( handleObj.selector ) { //同时delegateCount减去1 eventType.delegateCount--; } if (special.remove) { //处理个别事件移除 special.remove.call( elem, handleObj); } } } //如果已经移除所有此类问题,则卸载框架绑定去的elemData.handle //origCount !== eventType.length 是为了防止死循环 if ( eventType.length === 0 && origCount !== eventType.length) { if (!special.teardown || special.teardown.call(elem, namespaces, elemData.handle ) === false) { jQuery.removeEvent (elem, type, elemData.handle); } delete events[ type ]; } } //如果events为空,则从elemData中删除events与handler if ( jQuery.isEmptyObject( events )) { delete elemData.handle; jQuery.removeData (elem, "events", true) } }
事件卸载部分是jQuery事件系统中最简单的部分,主要逻辑都花在移除事件描述对象和匹配条件上。
八,jQuery.event.dispatch的源码解读
这是jQuery事件系统的核心。它就是利用这个dispatch方法,从缓存体中的events对象取得对于的队列,然后修复事件对象,逐个传入用户的回调中执行,根据返回值决定是否断开循环(stopImmediatePropagation),阻止默认行为和事件传播。
dispatch = function(event) { //创建一个伪事件对象(jQuery.Event实列),从真正的实际对象上抽得响应的属性附于其上 //如果是iE,也可以将其转换成对于的W3c属性,抹去两大平台的差异。 event = jQuery.event.fix(event || window.event); var i, j, cur, ret, selMatch, matched, matches, handleObj, sel, related, //取得所有事件描述对象 handlers = ((jQuery._data(this, "events") || {}) [ event.type ] || []), delegateCount = handlers.delegateCount, args = core_slice.call(arguments), run_all = !event.exclusive && !event.namespace, special = jQuery.event.special[ event.type ] || {}, handlerQueue = []; //重置第一个参数为jQuery.Event实例 args[0] = event; event.delegateTarget = this; //添加一个人为的属性,用于事件代理 //执行preDispatch回调,它与后面的postDispatch构成一种类似AOP的机制 if (special.preDispatch && special.preDispatch.call(this, event) === false) { return; } //如果是事件代理,并且不是来自非左键的点击事件 if (delegateCount && !(event.button && event.type === "click")) { //从事件源开始,遍历其所有祖先一直到绑定事件的元素 for ( cur = event.target; cur != this; cur = cur.parentNode || this) { //不要触发被disabled元素的点击事件 if (cur.disabled !== tru event.type !== "click") { selMatch = {}; //为了节能起见,每种CSS表达式只判定一次,通过下面的 //jQuery( sel, this).index( cur ) >= 0 或 jQuery.find(sel, this, null, [ cur ].length) matches = []; //用于收集符合条件的事件描述对象 //使用事件代理的事件描述对象总是排在最前面 for (i = 0; i < delegateCount; i++ ) { handleObj = handler[ i ]; sel = handleObj.selector; if (selMatch[ sel ] === undefined) { //有多少个元素匹配就收集多少个事件描述对象 selMatch[ sel] = handleObj.needsContext ? jQuery(sel, this).index(cur) >= 0 : jQuery.find(sel, this, null, [cur]).length; } if (selMatch[ sel ]){ matches.push(handleObj); } } if (matches.length) { handlerQueue.push({elem: cur, matches : matches}); } } } } //取得其他直接绑定的事件描述对象 if (handlers.length > delegateCount) { handlerQueue.push({elem : this, matches : handlers.slice(delegateCount) }) } //注意:这个循环是从下到上执行的 for (i = 0; i < handlerQueue.length && !event.isPropagationStopped(); i++) { matched = handlerQueue[ i ]; event.currentTarget = matched.elem; //执行此元素的所有与event.type同类型的回调,除非用户调用了stopImmediatePropagation方法,它会导致isImmediatePropagetionStopped返回true,从而中断循环 for (j = 0; j < matched.matches.length && !event.isImmediatePropagationStopped(); j++) { handleObj = matched.matches[j]; //最后的过滤条件为事件命名空间,比如著名的bootstrap的命名空间为data-api if (run_all || (!event.namespace && !handleObj.namespace) || event.namespace_re && event.namespace_re.test(handleObj.namespace)) { event.data = handleObj.data; event.handleObj = handleObj; //执行用户回调,(有时 可能还要外包一层,来自jQuery.event.specal[type].handle) ret = ((jQuery.event.special[ handleObj.origType ] || {}).handle || handleObj.handler).apply(matched.elem, args); //根据结果判定是否阻止事件传播与默认行为 if (ret !== undefined) { event.resulet = ret; if (ret === false) { // event.preventDefault(); event.stopPropagation(); } } } } } //执行postDispatch回调 if (special.postDispatch) { special.postDispatch.call(this, event); } return event.resulet; }
本节的难点在于如何模拟事件的传播机制,jQuery实际上只模拟冒泡那一阶段。
九,jQuery.event.trigger解读
笼统的来说,triggter就是dispatch的加强版。
dispatch只触发当前元素与其底下元素(通过事件代理的方式)的回调,trigger则模拟整个冒泡过程,除了它自身,还触发其祖先节点与window的同类型的回调。不过,从trigger的代码来看,它比dispatch多做的事情就是触发事件的默认行为。
这涉及到太多的判定,如果再把dispatch的代码写在一块就不好维护了。
但是,我觉得,trigger其实用不着那么复杂,trigger要做的事情就是在某一元素触发一个回调(dispatch),生产一个事件对象,然后让其顺序冒泡,触发其它的回调(dispatch)就行了。
浏览器提供了原生的派发机制,IE下的fireEvent,标准浏览器为dispatchEvent.
如果在控制台执行以下函数,我们会得到更多的事件构造器
Object.getOwnPropertyNames(window).filter(function (p) { return typeof window[p] == "function" && (window[p].prototype instanceof Event) })
["MIDIMessageEvent", "MIDIConnectionEvent", "MediaKeyMessageEvent", "MediaEncryptedEvent", "webkitSpeechRecognitionEvent", "webkitSpeechRecognitionError", "StorageEvent", "SpeechSynthesisEvent", "MediaStreamEvent", "IDBVersionChangeEvent", "GamepadEvent", "DeviceOrientationEvent", "DeviceMotionEvent", "CloseEvent", "OfflineAudioCompletionEvent", "AudioProcessingEvent", "MediaKeyEvent", "XMLHttpRequestProgressEvent", "WheelEvent", "WebGLContextEvent", "UIEvent", "TransitionEvent", "TrackEvent", "TouchEvent", "TextEvent", "SecurityPolicyViolationEvent", "SVGZoomEvent", "ProgressEvent", "PopStateEvent", "PageTransitionEvent", "MutationEvent", "MouseEvent", "MessageEvent", "MediaQueryListEvent", "KeyboardEvent", "HashChangeEvent", "FocusEvent", "ErrorEvent", "CustomEvent", "CompositionEvent", "ClipboardEvent", "BeforeUnloadEvent", "AutocompleteErrorEvent", "ApplicationCacheErrorEvent", "AnimationEvent", "WebKitAnimationEvent", "WebKitTransitionEvent"]
(webkit包含)
但常用的交互都集中在HTML4.0已经定义好的事件上,我们无需理会message storage popstate事件,更别提事件变动。我们认为支持以下事件就好了。
HTMLEvents | Load, unload, abort, error, select, change, submit, reset, focus, blur, resize, scroll |
KeyboardEvent | keypress keydown keyup |
MouseEvents | contextmenu, click, dblclick, mouseout, mouseover, mouseenter, mouseleave, mousemove,mousedown, mouseup,mousewheel |
根据上面的表格,我们做一个叫eventMap的hash出来,那么trigger方法最大限制级可以压缩成如下样子:
trigger: function (type, target) { var doc = target.ownerDocument || target.document || target || document ; event = doc.createEvent(eventMap[type] || "CustomEvent"); if (/^(focus|blur|select|submit|reset)$/.test(type)) { target[type] && target[type] (); //触发默认行为 } Event.initEvent( type, true, true); target.dispatchEvent(event); }
但是这样trigger出来的对象是只读,不能覆盖原生属性或方法,因此,你可以以为它自定义一个more属性,里边装载着你要改的东西,然后在dispatch将它包装成一个jQuery伪事件对象后,在把循环加在伪事件对象就行 了。
十,jQuery对事件对象的修复(不更新)
十一,滚轮事件的修复(不更新)
十二,mouseenter与mouseleave事件的修复(不更新)
十三,focus与focusout事件的修复(不更新)
十四,旧版本下IE的submit的事件代理实现(不更新)
在旧版本的IE下submit不会冒泡到顶层,它只执行form元素的submit回调,并立即执行提交跳转,因此只能用事件冒充的方式来实现。
我们看看什么情况下浏览器触发submit事件吧。submit事件与鼠标事件,键盘事件是不一样的。它是一种复合事件,既可以使用鼠标实现,也可以通过键盘事件实现,重要的结果,能实现表单提交即可。
当焦点聚焦于input[type=text]、input[type=password]、input[type=checkbox]、input[type=radio]、input[type=button]、input[type=image]、input[type=submit]、按回车键就会触发提交
当鼠标在input[type=image],input[type=submit]上方,通过点击事件就会触发提交
浏览器差异,IE下可以在input=file回车后触发
我们也可以使用form.submit()这样的编程手段触发
IE下submit, reset的事件代理,delegate用于检测它有没有用事件代理,不用管它。重点在于它在代理元素上一下绑定了两个事件,click,keypress,如果是键盘事件,就判断他的keycode是否为13108(回车键)
el.submit()方法有个特异之处,它是不会执行submit回调的,像其他click,blur,focus,select这样的DOM方法都会同时执行回调与默认行为。
十五,oninput事件的兼容性性处理(不更新)
(本章已更新完毕)