对于事件的操作无非是addEvent,fireEvent,removeEvent这三个事 件方法。一般lib都会对浏览器的提供的函数做一些扩展,解决兼容性内存泄漏等问题。第三个问题就是如何得到domReady的状态。
6.1 event的包裹
浏览器的事件兼容性是一个令人头疼的问题。IE的event在是在全局的window下, 而mozilla的event是事件源参数传入到回调函数中。还有很多的事件处理方式也一样。
Jquery提供了一个 event的包裹,这个相对于其它的lib提供的有点简单,但是足够使用。
代码如下:
//对事件进行包裹。
fix : function(event) {
if (event[expando] == true) return event;//表明事件已经包裹过
//保存原始event,同时clone一个。
var originalEvent = event; ①
event = { originalEvent : originalEvent};
for (var i = this.props.length, prop;i;) {
prop = this.props[--i];
event[prop] = originalEvent[prop];
}
event[expando] = true;
//加上preventDefault and stopPropagation,在clone不会运行
event.preventDefault = function() { ②
// 在原始事件上运行
if (originalEvent.preventDefault)
originalEvent.preventDefault();
originalEvent.returnValue = false;
};
event.stopPropagation = function() {
// 在原始事件上运行
if (originalEvent.stopPropagation)
originalEvent.stopPropagation();
originalEvent.cancelBubble = true;
};
// 修正 timeStamp
event.timeStamp = event.timeStamp || now();
// 修正target
if (!event.target) ③
event.target = event.srcElement || document;
if (event.target.nodeType == 3)//文本节点是父节点。
event.target = event.target.parentNode;
// relatedTarget
if (!event.relatedTarget && event.fromElement) ④
event.relatedTarget = event.fromElement == event.target
? event.toElement : event.fromElement;
// Calculate pageX/Y if missing and clientX/Y available
if (event.pageX == null && event.clientX != null) { ⑥
var doc = document.documentElement, body = document.body;
event.pageX = event.clientX
+ (doc && doc.scrollLeft || body && body.scrollLeft || 0)
- (doc.clientLeft || 0);
event.pageY = event.clientY
+ (doc && doc.scrollTop || body && body.scrollTop || 0)
- (doc.clientTop || 0);
}
// Add which for key events
if (!event.which && ((event.charCode || event.charCode === 0) ⑦
? event.charCode : event.keyCode))
event.which = event.charCode || event.keyCode;
// Add metaKey to non-Mac browsers
if (!event.metaKey && event.ctrlKey) ⑧
event.metaKey = event.ctrlKey;
// Add which for click: 1 == left; 2 == middle; 3 == right
// Note: button is not normalized, so don't use it
if (!event.which && event.button) ⑨
event.which = (event.button & 1 ? 1 : (event.button & 2
? 3 : (event.button & 4 ? 2 : 0)));
return event;
},
上面的代码①处保留原始事件的引用,同时clone原始事件。在这个clone的事件上进行包裹。②处在原始事件上运行 preventDefault 和 stopPropagation两个方法达到是否阻止默认的事件动作发生和是否停止冒泡事件事件向上传递。
③处是修正target个,IE中采用srcElement,同时对于文本节点事件,应该把target传到其父节点。
④处 relatedTarget只是对于mouseout、mouseover有用。在IE中分成了to和from两个Target变量,在mozilla中 没有分开。为了保证兼容,采用relatedTarget统一起来。
⑥处是进行event的坐标位置。这个是相对于page。如果页面 可以scroll,则要在其client上加上scroll。在IE中还应该减去默认的2px的body的边框。
⑦处是把键盘事件的按 键统一到event.which的属性上。Ext中的实现ev.charCode || ev.keyCode || 0; ⑨则是把鼠标事件的按键统一把event.which上。charCode、ev.keyCode一个是字符的按键,一个不是字符的按键。⑨处采 用&的方式来进行兼容性的处理。 Ext 通过下面三行解决兼容问题。
var btnMap = Ext.isIE ? {1:0,4:1,2:2} : (Ext.isSafari ? {1:0,2:1,3:2} : {0:0,1:1,2:2}); this.button = e.button ? btnMap[e.button] : (e.which ? e.which-1 : -1);
①②③④⑤⑥⑦⑧⑨⑩
6.2 事件的处理
Jquery提供了一些来进行regist,remove,fire事件 的方法。
6.2.1 Register
对于注册事件,jquery提供了bind、one、toggle、 hover四种注册事件的方法, bind是最基本的方法。One是注册只运行一次的方法,toggle注册交替运行的方法。Hover是注册鼠标浮过的方法。
代码如下:
bind : function(type, data, fn) {
return type == "unload" ? this.one(type, data, fn) : this
.each(function() {// fn || data, fn && data实现了data参数可有可无
jQuery.event.add(this, type, fn || data, fn && data);
}); },
Bind中对于unload的事件,只能运行一次,其它的就采用默认的注册方式。
// 为每一个匹配元素的特定事件(像click)绑定一个一次性的事件处理函数。
// 在每个对象上,这个事件处理函数只会被执行一次。其他规则与bind()函数相同。
// 这个事件处理函数会接收到一个事件对象,可以通过它来阻止(浏览器)默认的行为。
// 如果既想取消默认的行为,又想阻止事件起泡,这个事件处理函数必须返回false。
代码如下:
one : function(type, data, fn) {
var one = jQuery.event.proxy(fn || data, function(event) {
jQuery(this).unbind(event, one);
return (fn || data).apply(this, arguments);/this->当前的元素
});
return this.each(function() {
jQuery.event.add(this, type, one, fn && data);
});
},
One与bind基 本上差不多,不同的在调用jQuery.event.add时,把注册的事件处理的函数做了一个小小的调整。One调用了 jQuery.event.proxy进行了代理传入的事件处理函数。在事件触发调用这个代理的函数时,先把事件从cache中删除,再执行注册的事件函 数。这里就是闭包的应用,通过闭包得到fn注册的事件函数的引用。
//一个模仿悬停事件(鼠标移动到一个对象上面及移出这个对 象)的方法。
//这是一个自定义的方法,它为频繁使用的任务提供了一种“保持在其中”的状态。
//当鼠标移动到一个匹配的元素上面时,会 触发指定的第一个函数。当鼠标移出这个元素时,
/会触发指定的第二个函数。而且,会伴随着对鼠标是否仍然处在特定元素中的检测(例如,处在div 中的图像),
//如果是,则会继续保持“悬停”状态,而不触发移出事件(修正了使用mouseout事件的一个常见错误)。
hover : function(fnOver, fnOut) {
return this.bind('mouseenter', fnOver).bind('mouseleave', fnOut);
},
Hover则是建立在bind的基础之上。
//每次点击后依次调用函数。
toggle : function(fn) {
var args = arguments, i = 1;
while (i < args.length)//每个函数分配GUID
jQuery.event.proxy(fn, args[i++]);//修改后的还在args中
return this.click(jQuery.event.proxy(fn, function(event) {//分配GUID this.lastToggle = (this.lastToggle || 0) % i;//上一个函数 event.preventDefault();//阻止缺省动作
//执行参数中的第几个函数,apply可以采用array-like的参数
return args[this.lastToggle++].apply(this,arguments)||false;
}));
},
Toggle中参数可以是多个fn。先把它们代码生成UUID。之后调用click的方法来注册再次进行代理的callback。这个函数在事件触发时 运行,它先计算上一次是执行了参数中的那个函数。之后阻止缺省动作。之后找到下一个函数运行。
//为jquery对象增加常用 的事件方法
jQuery.each(
("blur,focus,load,resize,scroll,unload,click,dblclick,"
+ "mousedown,mouseup,mousemove,mouseover,mouseout,change,select,"
+ "submit,keydown,keypress,keyup,error").split(","),
function(i, name) {jQuery.fn[name] = function(fn) {
return fn ? this.bind(name, fn) : this.trigger(name);
};});
Jquery增加了一个常用的事件处理方法,包含上面调用的click。这里可以看出这里还是调用bind进行注册。当然这里还可以通过程序实现去触发 事件。
上面的众多方法都是注册事件,其最终都落在jQuery.event.add();来完成注册的功能。如果我们采用Dom0或DOM1 的事件方法,我们会采用elem.onclick=function(){}来为元素的某一种事件来注册处理函数。这个最大的缺点就是每个一个事件只是一 个处理函数。在dom1的方式中有改进,我们可以采用elem.addEventListener(type, handle, false)为元素的事件注册多个处理函数。
这样的处理方式还不是很完美,如果我们只这个事件运行一次就有点麻烦了。我们要在事件的处 理函数中最后进行elem.removeEventListener来取消事件的监听。这样做可能会有事务上的问题。如果第一个事件处理函数在没有取消事 件监听之前,就再次触发了怎么办?
还有采用浏览器的方式,它不支持自定义事件的注册和处理,还不能为多个事件注册同一个处理函数。
代码如下:
jQuery.event = {// add 事件到一个元素上。
add : function(elem, types, handler, data) {
if (elem.nodeType == 3 || elem.nodeType == 8) return;// 空白节点或注释
// IE不能传入window,先复制一下。
if (jQuery.browser.msie && elem.setInterval) elem = window;
// 为handler分配一个全局唯一的Id
if (!handler.guid) handler.guid = this.guid++;
// 把data附到handler.data中
if (data != undefined) { ①
var fn = handler;
handler =this.proxy(fn,function(){return fn.apply(this,arguments);});
handler.data = data;
}
// 初始化元素的events。如果没有取到events中值,就初始化data: {} ②
var events =jQuery.data(elem,"events")||jQuery.data(elem,"events",{}),
// 如果没有取到handle中值,就初始化data: function() {....} ③
handle = jQuery.data(elem, "handle")|| jQuery.data(elem, "handle",
function() {//处理一个触发器的第二个事件和当page已经unload之后调用一个事件。
if (typeof jQuery != "undefined"&& !jQuery.event.triggered)
return jQuery.event.handle.apply(//callee.elem=handle.elem
arguments.callee.elem, arguments);
});
// 增加elem做为handle属性,防止IE由于没有本地Event而内存泄露。
handle.elem = elem;
// 处理采用空格分隔多个事件名,如jQuery(...).bind("mouseover mouseout", fn);
jQuery.each(types.split(/s+/), function(index, type) { ④
// 命名空间的事件,一般不会用到。
var parts = type.split(".");type = parts[0];handler.type = parts[1];
// 捆绑到本元素type事件的所有处理函数
var handlers = events[type]; ⑤
if (!handlers) {// 没有找到处理函数列表就初始化事件队列
handlers = events[type] = {};
// 如果type不是ready,或ready的setup执行返回false ⑥
if (!jQuery.event.special[type]|| jQuery.event.special[type].setup
.call(elem, data) === false) {// 调用系统的事件函数来注册事件
if(elem.addEventListener)elem.addEventListener(type,handle,false);
else if (elem.attachEvent)elem.attachEvent("on" + type, handle);
}
}
// 把处理器的id和handler形式属性对的形式保存在handlers列表中,
// 也存在events[type][handler.guid]中。
handlers[handler.guid] = handler; ⑦
// 全局缓存这个事件的使用标识
jQuery.event.global[type] = true;
});
elem = null; // 防止IE内存泄露。
},
guid : 1,
global : {},
jQuery.event.add通过jQuery.data把事件相关的事件名和处理函数有机有序地组合起存放在 jQuery.cache中与该元素对应的空间里。我们就一个例子分析一下add的过程中:假如我们招待下面 jQuery(e1).bind("mouseover mouseout", fn0);jQuery(e1).bind("mouseover mouseout", fn1)的语句。
在jQuery(e1).bind("mouseover mouseout", fn0);时,②③都不可能从cache取到数,先初始化。此时的cache:{e1_uuid:{events:{},handle:fn}}。接着在 ⑤会为mouseover mouseout名初始化。此时的cache: {e1_uuid:{events:{ mouseover:{}, mouseout:{}},handle:fn}}。在⑥处向浏览器的事件中注册处理函数。接着⑦会把处理函数到事件名中。此时的cache: {e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}},handle:fn}}。这里可以看出为采用proxy为函数生成uuid的作用了。
在jQuery(e1).bind("mouseover mouseout", fn1)时,②③都从cache取到数据{e1_uuid:{events:{mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}},接着在⑤取到mouseover:{fn0_uuid:fn0},mouseout:{ fn0_uuid:fn0}的引用。接着⑦会把处理函数注册到事件名中。此时的cache: {e1_uuid:{events:{mouseover:{fn0_uuid:fn0, fn1_uuid:fn1,},mouseout:{ fn0_uuid:fn0, fn1_uuid:fn1}},handle:fn}}。
jQuery.event.add很重要的任务 就是把注册的事件函数有序地存放起来。以便remove和fire事件的函数能找到。
//{elem_uuid_1:{events:{mouseover:{fn_uuid:fn1,fn_uuid1:fn2},
//mouseout:{fn_uuid:fn1,fn_uuid1:fn2}},handle:fn}}
6.2.2 trigger
注册了事件,如onclick。那么当用户点击这个元素时,就会自动触发这个事件的已经注册的事件处理函数。但是我们有的时候要采用程 序来模拟事件的触发就得采用强迫触发某个事件。在IE中我们可以采用.fireEvent()来实现。如: