jQuery 源码:封装 Event
一、概述
本文围绕 "jQuery对DOM3 Event 接口的封装" 这一主题,分析DOM3 Event与 jQuery.Event 的异同,阐述 jQuery 对事件对象进行的封装和兼容原理。
二、Event
1. 标准事件模型的 Event 接口
在标准事件模型中,每一种事件都直接或间接实现了 Event 接口,这个接口定义了事件发生时向事件句柄传递的上下文信息,DOM2 Event 接口 定义如下:
// Introduced in DOM Level 2: interface Event { // PhaseType const unsigned short CAPTURING_PHASE = 1; const unsigned short AT_TARGET = 2; const unsigned short BUBBLING_PHASE = 3; readonly attribute DOMString type; readonly attribute EventTarget target; readonly attribute EventTarget currentTarget; readonly attribute unsigned short eventPhase; readonly attribute boolean bubbles; readonly attribute boolean cancelable; readonly attribute DOMTimeStamp timeStamp; void stopPropagation(); void preventDefault(); void initEvent(in DOMString eventTypeArg, in boolean canBubbleArg, in boolean cancelableArg); };
DOM3 Event 接口在此基础上增加了两个属性和一个函数:
// Introduced in DOM Level 3: void stopImmediatePropagation(); readonly attribute boolean defaultPrevented; readonly attribute boolean isTrusted;
其中, stopImmediatePropagation() 用于阻止剩余的事件侦听函数执行;defaultPrevented 用于表示事件的默认行为是否已被取消,当cancelable为true并且调用了preventDefault()时,defaultPrevented应该为true,否则为false;isTrusted 为true表示该事件是由user agent产生的,否则是由脚本手动生成的,不可信。
2. Jquery 的 Event 接口
2.1 实现的是旧接口
Jquery 宣称自己的 jQuery.Event 接口实现了 DOM3 的 Event 接口,不过,根据代码里提供的链接,它所 参照的Event 是2003年的一份草案,该文档已经过时。直到版本1.9.1,Jquery一直在模拟那个旧草案里的3个函数,即 isDefaultPrevented() ,isPropagationStopped() ,isImmediatePropagationStopped() 。而对新标准里的 defaultPrevented 属性却没有模拟。
2.2 只用一个类型
在标准事件模型中,各事件接口存在以下继承关系:
各类鼠标事件对象是 MouseEvent 对象,键盘事件对象则是 KeyboardEvent 对象。"在 IE 和 jQuery 事件模型中,Element 对象上发生的所有事件都只使用一个单独的 Event 对象"(见《javascript权威指南》第六版第四部分Event条目)。
2.3 jQuery 如何模拟 Event
对原生 event 对象的封装,是由 jQuery.event.fix() 和 jQuery.Event 构造函数协作完成的。调用 fix 函数并传入原生event对象,可以获得一个修复后的 jQuery.Event 对象。版本 1.6.3 中,获得的对象从原生对象中拷贝了 37 种以上的属性,然后对部分属性进行修复:
"altKey attrChange attrName bubbles button cancelable charCode clientX clientY ctrlKey currentTarget data detail eventPhase fromElement handler keyCode layerX layerY metaKey newValue offsetX offsetY pageX pageY prevValue relatedNode relatedTarget screenX screenY shiftKey srcElement target toElement view wheelDelta which"
由于不同版本的 jQuery 对 Event 和 fix() 的实现存在差异,笔者只阐述兼容方法,不详解所有源代码。
2.3.1 属性修复
1)target
在 ie 下为 event.srcElement。此外,在 safari 中,textnode(nodeType为3)也可能成为 target,此时,用 target.parentNode 作为target。
2)relatedTarget
IE 不支持 relatedTarget。fromElement 和 toElement 出现于 IE 的 mouseover 和 mouseout 事件对象中。当 fromElement === target(修复后的target) 时,表明是 mouseout 事件,可以用 toElement 代替 relatedTarget ,否则是 mouseover 事件,用 fromElement 代替。
3)pageX 与 pageY
IE 不支持这两个属性,表示文档坐标。不过标准事件模型与IE事件模型都支持 clientX 和 clientY ,二者表示窗口坐标,可以通过窗口坐标转换为文档坐标,以pageY为例,等于clientY 加上文档的滚动高度,减去文档的 clientTop(可以近似理解为文档的上边框宽度,如果滚动条出现在文档顶部,则等于上边框宽度加上滚动条宽度)。注意,document.documentElement.clientTop 在IE7下存在兼容问题,如果使用 Transitional 或者 html5 的 DOCTYPE 并且不设置 <html> 的上边框,则 clientTop 为 2px , 不过 jQuery 的代码未做处理,可能会有问题。为方便理解,笔者不直接使用源码,模拟如下:
//传入原生 evt 对象 function getPageXY(evt) { var doc , html , body , target = evt.target || evt.srcElement , ret = {}; if (evt.pageX != null) { ret.pageX = evt.pageX; ret.pageY = evt.pageY; } else if (evt.clientX != null) { doc = target.ownerDocument; html = doc.documentElement; body = doc.body; ret.pageX = evt.clientX + (html.scrollLeft || body.scrollLeft || 0) - (html.clientLeft || body.clientLeft || 0); ret.pageY = evt.clientY + (html.scrollTop || body.scrollTop || 0) - (html.clientTop || body.clientTop || 0); } return ret; }
4)which
非标准属性,IE 不支持。
鼠标事件中,which 为1表示左键,2中键,3右键。
键盘事件中,which与keyCode取值一致。
鼠标事件中,可利用 IE button 得到 which ,IE 的 button 是个位图,1表示左键,2表示中键,4表示右键。
键盘事件中,可以用keyCode模拟which,但是 Firefox 的按键事件不支持 keyCode,只能用 charCode 模拟keyCode。
笔者对 jQuery 实现模拟如下:
//evt 为原生 Event 对象 function getWhich(evt) { if(! evt.which) { if(evt.button !== undefined) {//IE 鼠标事件 return (evt.button & 1) ? 1 : //左键 (evt.button & 2) ? 2 : //中键 (evt.button & 4) ? 3 : //右键 0; //未知键 }else if(evt.keyCode != null){//非FF,键盘事件 return evt.keyCode; }else if(evt.charCode != null){//FF, 键盘事件 return evt.charCode; } } return null; }
5)metaKey
在MAC浏览器中,jQuery 把 ctrlKey 作为metaKey
6)timeStamp
此属性收操作系统浏览器影响,jQuery 使用生成该事件对象的时间来替代, (new Date()).getTime() 。
2.3.2 函数修复
首先指出,jQuery 内部定义两个函数:
function returnFalse() { return false; } function returnTrue() { return true; }
1)isDefaultPrevented()
fix 函数根据原生 event 对象的默认行为是否已经被阻止来设置这个函数,如果已经被阻止,则设置为 returnTrue 否则为 returnFalse 。调用经过包装的 preventDefault() 函数会设置此函数为 returnTrue 。jQuery 如何判断默认行为已经被阻止了呢,给定原生事件对象 src ,则 jQuery 用以下代码模拟该函数:
(src.defaultPrevented || src.returnValue === false || src.getPreventDefault && src.getPreventDefault()) ? returnTrue : returnFalse;
注意到其中的 getPreventDefault() 函数是非标准的已经被废弃的函数,用于支持旧版的火狐和其它也使用Gecko内核的浏览器(ThunderBird SeaMonkey)。
2)isPropagationStopped()
当调用封装后的 stopPropagation() 时,此函数会被设置为 returnTrue ,否则默认为 returnFalse。注:此函数已经被DOM3废弃。
3)isImmediatePropagationStopped()
默认为 returnFalse。当调用 stopImmediatePropagation() 时,被设置为 returnTrue。
4)preventDefault()
调用标准的 preventDefault() 函数 或者 设置非标准的 returnValue 为 false 来做兼容处理。
5)stopPropagation()
调用标准的 stopPropagation() 函数 或者 设置非标准的 cancelBubble 为 true 来做兼容处理。
6)stopImmediatePropagation()
设置 isImmediatePropagation 句柄为 returnTrue ,并调用兼容后的 stopPropagation() 函数。
2.3.3 命名空间
jQuery 还扩展了 Event 接口,加入了 namespace 属性,绑定事件的时候,可以指定命名空间,手动触发的时候可以认为指定事件发生的命名空间。参考 jQuery 的 bind 和 trigger 函数。
2.3.4 自定义事件类型与任意扩展属性
实际上 jQuery.Event 的第二个参数可以是一个对象,其中的的属性将被整合到新生成的 jQuery.Event 对象中,这个自定义事件提供了灵活性。自定义事件的绑定与触发在本文其余部分会提到。