最佳的addEvent事件绑定是怎样诞生的

时间:2021-05-18

当我们编写脚本的时候创建了交叉引用,例如如下代码:

window.onload = function() { var x = document.getElementsByTagName('H3'); for (var i = 0; i < x.length; i++) { x[i].onclick = openClose; x[i].relatedElement = x[i].nextSibling; // simplified situation x[i].relatedElement.relatedElement = x[i]; }}

或者在函数中使用脚本语言最常见的闭句Closures的时候,IE都无法回收内存。而闭句在给DOM对象注册事件处理器(event handler)的时候最为常用。Novemberborn提供了一些example可以让你运行并切实感受到这个bug。
我最喜爱的QuirkMode 去年初意识到这个bug存在巨大隐患,觉得有必要呼吁广大web开发者关注并竭力避免这个问题,于是举办了一个慈善邀请赛,鼓励大家提交各自 addEvent/removeEvent 方案。并终于在去年10月下旬宣布了他们认为的胜利者:John Resig,让John赢得胜利的代码如下:

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);}function removeEvent(obj, type, fn) { if (obj.detachEvent) { obj.detachEvent('on' + type, obj[type + fn]); obj[type + fn] = null; } else obj.removeEventListener(type, fn, false);}

QuirkMode 对选择John为胜利者的解释概括来说就是以上代码最简洁有效,在避免内存问题的同时还巧妙的保证了this关键字在ie的attachEvent中能正常工作。缺点当然还是存在:

不支持 Netscape 4 和 Explorer 5 Mac。(有可能国内的程序员会嗤之以鼻,但国外很强调广泛的兼容性)
在 removeEvent 中遗漏了remove obj["e"+type+fn]。
总之不管怎么说,简单取胜。
结果一出,众多参赛与评论者不服气,很快又挑出了John的代码的几处毛病:

addEvent中本身就使用了闭句,所以没有根本解决IE内存泄露的问题。
没有解决同类型的事件可能被重复注册而被IE重复执行的问题。
几个高手于是提出了改进性的方案:

function addEvent(obj, type, fn) { if (obj.addEventListener) obj.addEventListener(type, fn, false); else if (obj.attachEvent) { obj["e" + type + fn] = fn; obj.attachEvent("on" + type, function() { obj["e" + type + fn](); }); }}function removeEvent(obj, type, fn) { if (obj.removeEventListener) obj.removeEventListener(type, fn, false); else if (obj.detachEvent) { obj.detachEvent("on" + type, obj["e" + type + fn]); obj["e" + type + fn] = null; }}

很明显,虽然修正了John代码的一些不足。但内存泄露依然存在,部分浏览器依然不支持,还是无法避免ie重复注册。另外根据注释:当在同一个对象上注册多个事件处理器的时候,IE与其他浏览器的执行顺序是不同的,这又是一个隐患。

几天之后,一个被认为最严谨的方案由Dean Edwards 提出。Dean他的方案与众不同:

不执行对象检测(Object detection)
没有调用 addeventListener/attachEvent 方法
保持this关键字的运行于正确的上下文环境
正确传递 event 对象参数
完全跨浏览器至此(包括IE4和NS4)
不存在内存泄露

Dean的代码如下:

// written by Dean Edwards, 2005 // http://dean.edwards.name/function ;addEvent(element, type, handler) { // 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) { // delete the event handler from the hash table if (element.events && element.events[type]) { delete element.events[type][handler.$$guid]; }};function handleEvent(event) { // grab the event object (IE uses a global event object) event = event || window.event; // get a reference to the hash table of event handlers // 这里的 this 随 handlerEvent function 被触发的source element 变化而变化 var handlers = this.events[event.type]; // execute each event handler for (var i in handlers) { //这样写才能保证注册的事件处理函数中的 this 得到正确的引用,直接handlers[i]()是不行的 this.$$handleEvent = handlers[i]; this.$$handleEvent(event); }};

这段代码相比之前就大了不少了,不过确实很精妙。可是这段代码却引入了其他的问题,比如无法处理事件处理函数的返回值,for..in循环可能因为 (Object.prototype)的错误应用而中断等等...很快Dean推出一个"updated version"。

要做到最好真的好辛苦。

目前似乎Dean的最终版本是最全面的解决方案。不过就我个人意见,感觉有些吹毛求疵了。尽量使用浏览器本身的实现和保持简单是我一贯坚持的主张。但洋人这种严谨的态度,还是让我深深敬佩。

声明:本页内容来源网络,仅供用户参考;我单位不保证亦不表示资料全面及准确无误,也不保证亦不表示这些资料为最新信息,如因任何原因,本网内容或者用户因倚赖本网内容造成任何损失或损害,我单位将不会负任何法律责任。如涉及版权问题,请提交至online#300.cn邮箱联系删除。

相关文章