浏览器不仅具有不同的事件和事件流处理方式,而且它们注册事件侦听器的方式也不一样。
1. 嵌入式注册模型
例如想下面代码演示的嵌入式的事件侦听器:
<a href="http://example.com" onclick="window.open(this.href); return false;">http://example.com</a>
在事件的概念刚刚出现时,嵌入式事件注册是唯一可用的方法。它需要在标记中为每个事件作为HTML属性进行硬编码,从而 需要为每个元素编写相同的代码,这不仅导致标记混乱,而且还会使文件尺寸变大。
2. 深入理解ADS.addEvent()方法
到目前为止一直使用自定义的ADS.addEvent()方法将事件侦听器添加到DOM元素上。这个方法的最早版本是由Scott Andrew LePera((http://www.scottandrew.com/weblog/articles/cbs-events)开发的,是一个将W3C和Microsoft不同事件模型组合到一个函数中的方法:
function addEvent(obj, evType, fn, useCapture){ if (obj.addEventListener){ obj.addEventListener(evType, fn, useCapture); return true; } else if (obj.attachEvent){ var r = obj.attachEvent("on"+evType, fn); return r; } else { alert("Handler could not be attached"); } }
这个addEvent()方法的实现有一些问题:
(1) 由于Microsoft事件模型不允许捕获,因此其中的useCapture参数显得有点模糊
(2) 侦听器环境下this关键字在IE和W3C中也是不同的
自定义库ADS中创建的addEvent()方法实在John Resig(http://ejohn.org/projects/flexible-javascript-events/)版本基础上修改完成的:
function addEvent( obj, type, fn ) { if ( obj.attachEvent ) { obj['e'+type+fn] = fn; obj[type+fn] = function(){obj['e'+type+fn]( window.event );} obj.attachEvent( 'on'+type, obj[type+fn] ); } else{ obj.addEventListener( type, fn, false ); } }
(1) 这个版本去掉了useCapture参数
(2) 将DOM的默认方式设置为事件冒泡(与IE相似)
(3) 通过使用匿名函数使this关键字在Microsoft和W3C环境中保持一致,this引用的是将侦听器指派给的对象
3. 传统事件模型
使用传统方法注册事件侦听器时就是定义事件触发时执行的方法,然后将这个方法指定给对象的事件侦听器属性:
// 传统的事件注册方式 window.onload = function() { var anchor = document.getElementById('example'); anchor.onclick = function(){ // 触发单击事件时执行的代码 } }
传统方法是最直观的也是不针对特定浏览器的事件注册方法,但这种方法也并不完美:
(1) 传统事件注册方法中的this关键字引用的是目标对象
(2) 事件侦听器只能是一个单独的函数。当然如果想调用多个侦听器,就必须将多个侦听器包装在一个函数体内
(3) 传统方法从属于浏览器默认的事件流,没有办法指定是在捕获阶段还是在冒泡阶段
4. Microsoft特色的事件模型
在Microsoft的解决方案中,分别使用attachEvent()和detachEvent()方法注册和移除事件侦听器。要在具体的对象上注册事件侦听器,必须首先定义一个函数,然后使用该对象的attachEvent(event,listener)方法注册事件。而事件的名称仍然使用与传统方法相同的on前缀:
// Microsoft事件注册 function eventListener() { // 响应事件的代码 } window.attachEvent('onload', function() { var link = document.getElementById('example'); link.attachEvent('onclick',eventListener); });
此后可以使用相同参数的detachEvent()方法移除事件侦听器:
link.detachEvent('onclick',eventListener);
使用Microsoft的方法,也可以为同一个对象指定多个事件侦听器:
link.attachEvent('onclick', eventListenerA); link.attachEvent('onclick', eventListenerB); link.attachEvent('onclick', eventListenerC);
而且,还可以使用fireEvent()方法手工调用事件:
link.fireEvent("onclick");
虽然Microsoft的方法比传统方法更清晰,但也有一些问题:
(1) 只对IE有效
(2) 与传统的模型不同,Microsoft的模型只是引用而非复制事件侦听器,因此在使用this关键字时,this引用的将是JavaScript函数,而不是注册事件侦听器的那个对象
(3) IE不支持捕获。除非只有cancelBubble属性来阻止冒泡,否则所有事件始终会冒泡
5. W3C DOM2事件模型
DOM2级事件规范中包含addEventListener()和removeEventListener()方法,这两个方法接受事件和事件侦听器参数,同时还允许通过第3个参数指定事件阶段。W3C去掉了on前缀的方案,因此所有事件必须使用事件名称而非传统的方法名称来标识:
// W3C事件注册 function eventListener() { // 响应单击事件的代码 } window.addEventListener('load', function(W3CEvent) { var link = document.getElementById('example'); link.addEventListener('click',eventListener,false); }, false);
第3个参数如果是true,事件侦听器将在捕获阶段内执行;如果是false,则会在冒泡阶段触发。
可以使用removeEventListener()方法移除事件侦听器:
link.removeEventListener('click',eventListener,false);
而且,还可以为同一个对象添加任意多个事件侦听器:
link.addEventListener('click',eventListenerA,false); link.addEventListener('click',eventListenerB,false); link.addEventListener('click',eventListenerC,false);
在W3C模型中,也可以通过组合document.createEvent()方法和对象的dispatchEvent()方法来手工调用事件。例如要模仿一次单击事件,需要创建一个MouseEvent事件,并且在分派给DOM元素之前初始化:
// 使用W3C的方法手工调用事件 var event = document.createEvent("MouseEvents"); event.initMouseEvent( 'click', // 事件类型 true, // 可以冒泡 true, // 可以取消默认动作 window, // 视图类型 0, // 鼠标点击数 0, // 屏幕的x坐标 0, // 屏幕的y坐标 0, // 客户端的x坐标 0, // 客户端的y坐标 false, // 是否按下Ctrl键 false, // 是否按下Alt键 false, // 是否按下Shift键 false, // 是否按下Meta键 0, // 按下鼠标的次数 null // 相关的目标对象 ); anchor.dispatchEvent(evt);
6. load事件的问题
无论使用哪种事件注册方法,通过window对象的load事件来初始化DOM脚本都会存在一个固有的问题:当页面总包含许多大文件时,load事件会一直等到所有资源全部载入完毕后才会被触发。如果创建的应用程序需要使用嵌入的图像,那么可能会希望load事件在嵌入的图像载入完成之前运行。要解决这个问题,需要在自定义的ADS库中添加ADS.addLoadEvent()方法:
/** * 注册load事件,在页面载入完成之后并在图像载入完成之前运行 */ function addLoadEvent(loadEvent,waitForImages) { if(!isCompatible()) return false; // 如果等待标记是true,则使用常规的事件注册方法 if(waitForImages) { return addEvent(window, 'load', loadEvent); } // 否则包装loadEvent()方法 // 为this关键字指定正确的内容 // 确保事件不会执行两次 var init = function() { // 如果函数已经被调用了,则返回 if (arguments.callee.done) return; // 标记函数已经运行 arguments.callee.done = true; // 在document环境中运行载入事件 loadEvent.apply(document,arguments); }; // 为DOMContentLoaded事件注册事件侦听器 if (document.addEventListener) { document.addEventListener("DOMContentLoaded", init, false); } // 对于Safari使用setInterval()函数检测document是否载入完成 if (/WebKit/i.test(navigator.userAgent)) { var _timer = setInterval(function() { if (/loaded|complete/.test(document.readyState)) { clearInterval(_timer); init(); } },10); } // 对于IE(使用条件注释) // 附加一个最后执行的脚本,并检测该脚本是否载入完成 /*@cc_on @*/ /*@if (@_win32) document.write("<script id=__ie_onload defer src=javascript:void(0)><\/script>"); var script = document.getElementById("__ie_onload"); script.onreadystatechange = function() { if (this.readyState == "complete") { init(); } }; /*@end @*/ return true; } window['ADS']['addLoadEvent'] = addLoadEvent;
这个改进方法是在Dean Edwards()论述的解决方案为基础的,其中使用了不同的方法在图像完成载入之前调用DOM脚本:
□ 如果浏览器中存在addEventListener()方法,则使用DOMContentLoaded事件,该事件会在文档标记载入完成时调用
□ 对于Safari,则使用setInterval()函数周期性地检查document的readyState属性,随时监控文档是否载入完成
□ 对于IE,向文档中写入一个新的script标签,但该标签会延迟到文件最后载入。然后,使用script对象的onreadystatechange方法检查readyState属性
键ADS.addLoadEvent()方法的第2个参数设置为true,则可以调用包含在其它的原始的ADS.addEvent(window,’load’…)方法。
示例:加载一个580KB的JPG图像,使用的3个载入事件是:
// 使用常规的addEvent()方法为window对象注册load事件 ADS.addEvent(window,'load',function(W3CEvent) { ADS.log.write('ADS.addEvent(window,load,...) invoked'); }); // 使用改进后的addLoadMethod()方法 ADS.addLoadEvent(function(W3CEvent) { ADS.log.write('ADS.addLoadEvent(...) invoked'); }); // 通过改进后的addLoadMethod()方法调用原始的addEvent()方法 ADS.addLoadEvent(function(W3CEvent) { ADS.log.write('ADS.addLoadEvent(...,true) invoked'); },true);
当加载页面后,中间的ADS.addLoadMethod()方法会先于图像载入完成触发,而第一个和最后一个都需要一直等到最后才会触发。如下图:
转载请注明:陈童的博客 » ADS4.3 响应用户操作和事件——注册事件