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


Posted in Javascript onOctober 24, 2011

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

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重复执行的问题。
几个高手于是提出了改进性的方案:

/* 
Original idea by John Resig 
Tweaked by Scott Andrew LePera, Dean Edwards and Peter-Paul Koch 
Fixed for IE by Tino Zijdel (crisp) 
Note that in IE this will cause memory leaks and still doesn't quite function the same as in browsers that do support the W3C event model: 
- event execution order is not the same (LIFO in IE against FIFO) 
- functions attached to the same event on the same element multiple times will also get executed multiple times in 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的最终版本是最全面的解决方案。不过就我个人意见,感觉有些吹毛求疵了。尽量使用浏览器本身的实现和保持简单是我一贯坚持的主张。但洋人这种严谨的态度,还是让我深深敬佩。

Javascript 相关文章推荐
JavaScript按位运算符的应用简析
Feb 04 Javascript
简单介绍JavaScript中字符串创建的基本方法
Jul 07 Javascript
利用Angularjs和原生JS分别实现动态效果的输入框
Sep 01 Javascript
Angularjs之filter过滤器(推荐)
Nov 27 Javascript
原生js实现轮播图
Feb 27 Javascript
bootstrap 设置checkbox部分选中效果
Apr 20 Javascript
给Easyui-Datebox设置隐藏或者不可用的解决方法
May 26 Javascript
浅谈ES6新增的数组方法和对象
Aug 08 Javascript
详解如何在React组件“外”使用父组件的Props
Jan 12 Javascript
从零开始用electron手撸一个截屏工具的示例代码
Oct 10 Javascript
JavaScript中将值转换为字符串的五种方法总结
Jun 06 Javascript
Node如何后台数据库使用增删改查功能
Nov 21 Javascript
关于javascript function对象那些迷惑分析
Oct 24 #Javascript
文本框根据输入内容自适应高度的代码
Oct 24 #Javascript
js创建数据共享接口——简化框架之间相互传值
Oct 23 #Javascript
javascript模版引擎-tmpl的bug修复与性能优化分析
Oct 23 #Javascript
js面向对象设计用{}好还是function(){}好(构造函数)
Oct 23 #Javascript
jQuery EasyUI API 中文文档 - TimeSpinner时间微调器
Oct 23 #Javascript
利用jQuery插件扩展识别浏览器内核与外壳的类型和版本的实现代码
Oct 22 #Javascript
You might like
编写漂亮的代码 - 将后台程序与前端程序分开
2008/04/23 PHP
php面向对象全攻略 (二) 实例化对象 使用对象成员
2009/09/30 PHP
Swoole-1.7.22 版本已发布,修复PHP7相关问题
2015/12/31 PHP
使用Modello编写JavaScript类
2006/12/22 Javascript
用函数式编程技术编写优美的 JavaScript_ibm
2008/05/16 Javascript
JS注册/移除事件处理程序(ExtJS应用程序设计实战)
2013/05/07 Javascript
js控制input框只读实现示例
2014/01/20 Javascript
JavaScript中判断整数的多种方法总结
2014/11/08 Javascript
在windows上用nodejs搭建静态文件服务器的简单方法
2016/08/11 NodeJs
jQuery实现的导航下拉菜单效果示例
2016/09/05 Javascript
防止重复发送 Ajax 请求
2017/02/15 Javascript
详解通过JSON数据使用VUE.JS
2017/05/26 Javascript
微信小程序 es6-promise.js封装请求与处理异步进程
2017/06/12 Javascript
Easyui在treegrid添加控件的实现方法
2017/06/23 Javascript
React-Native做一个文本输入框组件的实现代码
2017/08/10 Javascript
angular中不同的组件间传值与通信的方法
2017/11/04 Javascript
vue项目中导入swiper插件的方法
2018/01/30 Javascript
关于node-bindings无法在Electron中使用的解决办法
2018/12/18 Javascript
jQuery实现表格的增、删、改操作示例
2019/01/27 jQuery
微信小程序接入腾讯云验证码的方法步骤
2020/01/07 Javascript
[49:31]DOTA2-DPC中国联赛 正赛 Elephant vs LBZS BO3 第二场 1月29日
2021/03/11 DOTA
[46:20]DOTA2-DPC中国联赛 正赛 PSG.LGD vs LBZS BO3 第二场 1月22日
2021/03/11 DOTA
Django Admin 实现外键过滤的方法
2017/09/29 Python
python爬虫爬取网页表格数据
2018/03/07 Python
python如何拆分含有多种分隔符的字符串
2018/03/20 Python
python实现opencv+scoket网络实时图传
2020/03/20 Python
解决python便携版无法直接运行py文件的问题
2020/09/01 Python
HTML高亮关键字的实现代码
2018/10/22 HTML / CSS
Html5原生拖拽相关事件简介以及基础实现
2020/11/19 HTML / CSS
惊艳的手工时装首饰:Migonne Gavigan
2018/02/23 全球购物
英国Office鞋店德国网站:在线购买鞋子、靴子和运动鞋
2018/12/19 全球购物
丑小鸭教学反思
2014/02/03 职场文书
聚美优品恶搞广告词
2014/03/14 职场文书
会计求职自荐信
2014/06/20 职场文书
助学感谢信范文
2015/01/21 职场文书
python flappy bird小游戏分步实现流程
2022/02/15 Python