js便签笔记(5)——Dean Edwards大牛的跨浏览器AddEvent()设计(不知道是不是jQuery事件系统的原型)
1. 前言:
在看Aaron的jquery源码解读时候,看到事件系统那块,作者提到了Dean Edwards的添加事件的设计,于是就点进去看了看。首先让我吃惊的是,代码非常少,寥寥几十行,非常简单。于是我就仔细的看了看(如果代码太多,可能就直接不看了)。
这段代码是Dean Edwards在2005年写的了,那时候还没有jquery。但是它的设计思路确实和jquery的事件系统有些相似,即便是在9年之后的今天。
于是把这段代码仔细研究,并在此跟大家分享以下。它将帮助你更好的理解jquery的事件系统。
先把源码粘上,:
1 function addEvent(element, type, handler) { 2 // assign each event handler a unique ID 3 if (!handler.$$guid) handler.$$guid = addEvent.guid++; 4 // create a hash table of event types for the element 5 if (!element.events) element.events = {}; 6 // create a hash table of event handlers for each element/event pair 7 var handlers = element.events[type]; 8 if (!handlers) { 9 handlers = element.events[type] = {}; 10 // store the existing event handler (if there is one) 11 if (element["on" + type]) { 12 handlers[0] = element["on" + type]; 13 } 14 } 15 // store the event handler in the hash table 16 handlers[handler.$$guid] = handler; 17 // assign a global event handler to do all the work 18 element["on" + type] = handleEvent; 19 }; 20 // a counter used to create unique IDs 21 addEvent.guid = 1; 22 23 function removeEvent(element, type, handler) { 24 // delete the event handler from the hash table 25 if (element.events && element.events[type]) { 26 delete element.events[type][handler.$$guid]; 27 } 28 }; 29 30 function handleEvent(event) { 31 // grab the event object (IE uses a global event object) 32 event = event || window.event; 33 // get a reference to the hash table of event handlers 34 var handlers = this.events[event.type]; 35 // execute each event handler 36 for (var i in handlers) { 37 this.$$handleEvent = handlers[i]; 38 this.$$handleEvent(event); 39 } 40 };
2. 该设计的优点:
Dean Edwards在文章中提到了该设计的几个优点:
it performs no object detection 不执行对象检测,不理解何意。。。
it does not use the addeventListener
/attachEvent
methods 不使用addeventListener
/attachEvent
方法,因为这两个方法分别由不同的浏览器支持,使用时候需要判断。 但是,在Dean Edwards提供的下载代码中,应用到了addeventListener。
it keeps the correct scope (the this
keyword) 保持正确的作用域,即this关键字
it passes the event object correctly 正确的传递event对象
it is entirely cross-browser (it will probably work on IE4 and NS4) 保证浏览器兼容性,甚至支持IE4和NetSape4(2005年)
and from what I can tell it does not leak memory 不会出现内存泄漏
3. 代码解读:
3.1 事件添加方法addEvent():
1 //事件添加方法 2 function addEvent(element, type, handler) { 3 4 // assign each event handler a unique ID 5 // 为传入的每个事件初始化一个唯一的id 6 if (!handler.$$guid) handler.$$guid = addEvent.guid++; //下文:addEvent.guid = 1; 7 8 // create a hash table of event types for the element 9 // 给element维护一个events属性,初始化为一个空对象。 10 // element.events的结构类似于 { "click": {...}, "dbclick": {...}, "change": {...} } 11 // 即element.events是一个对象,其中每个事件类型又会对应一个对象 12 if (!element.events) element.events = {}; 13 14 // create a hash table of event handlers for each element/event pair 15 // 试图取出element.events中当前事件类型type对应的对象,赋值给handlers 16 var handlers = element.events[type]; 17 if (!handlers) { 18 handlers = element.events[type] = {}; 19 //如果handlers是undefined,则初始化为空对象 20 21 22 // store the existing event handler (if there is one) 23 // 如果这个element已经有了一个方法,例如已经有了onclick方法 24 // 就把element的onclick方法赋值给handlers的0元素,此时handlers的结构就是: 25 // { 0: function(e){...} } 26 // 此时element.events的结构就是: { "click": { 0: function(e){...} }, /*省略其他事件类型*/ } 27 if (element["on" + type]) { 28 handlers[0] = element["on" + type]; 29 } 30 } 31 // store the event handler in the hash table 32 // 把当前的事件handler存放到handlers中,handler.$$guid = addEvent.guid++; addEvent.guid = 1; 肯定是从1开始累加的 33 // 因此,这是handlers的结构就是 { 0: function(e){...}, 1: function(){}, 2: function(){} 等等... } 34 handlers[handler.$$guid] = handler; 35 36 // assign a global event handler to do all the work 37 // 下文定义了一个handleEvent(event)函数 38 // 将这个函数,绑定到element的type事件上。 说明:在element进行click时,将会触发handleEvent函数,handleEvent函数将会查找element.events,并调用相应的函数。可以把handleEvent称为“主监听函数” 39 element["on" + type] = handleEvent; 40 }; 41 42 // a counter used to create unique IDs 43 addEvent.guid = 1;
以上代码都给出了详细的注释,应该能看明白了。重新梳理以下数据结构,经过addEvent()函数之后,当前的数据结构为:(假如type = 'click')
element: { onclick: handleEvent(event), /*下文定义的函数*/ events: { click:{ 0: function(){...}, /*element已有的click事件*/ 1: function(){...}, 2: function(){...} /*.......其他事件......*/ }, change:{ /*省略*/ }, dbclick:{ /*省略*/ } } }
这样的设计,其实已经具备了jquery事件系统的雏形,包含了两个最主要的特点:
- element上的所有事件,将保存到element.events属性中,不是直接绑定到element上;
- handleEvent作为element所有事件的“主监听函数”,有它统一管理element上的所有函数。
接着往下看:
3.2 主监听函数handleEvent(event):
1 //主监听函数 2 function handleEvent(event) { 3 // grab the event object (IE uses a global event object) 4 // 在IE中,event需要通过window.event获取 5 event = event || window.event; 6 7 // get a reference to the hash table of event handlers 8 // 根据事件类型在events中获取事件集合(events的数据结构,参考addEvent方法的注释) 9 var handlers = this.events[event.type]; 10 // 注意!注意! 这里的this不是window,而是element对象,因为上文 element["on" + type] = handleEvent; 11 // 所以在程序执行时,handleEvent已经作为了element的一个属性,它的作用域是element,即this === element 12 13 // execute each event handler 14 // 循环执行handlers集合里的所有函数 另外,这里执行事件时传递的event,无论在什么浏览器下,都是正确的 15 for (var i in handlers) { 16 this.$$handleEvent = handlers[i]; 17 this.$$handleEvent(event); 18 19 //此处为何要把handlers[i]赋值给this.$$handleEvent,然后在执行呢? 20 //而不是直接执行handlers[i](event)? 21 //跟内存泄漏有关? 22 //我也没看明白,大家自己思考的,知道的可以分享给大家。 23 } 24 };
以上就是主监听函数的实现过程,都做了注释,也不叫好理解,有个问题,已经在代码中有黄色背景标出来了,有了解的,也麻烦分享给大家。
jquery的主监听函数执行时候实现的比较复杂,但是思路上和这个是一样的。
3.3 移除函数事件:
1 //移除函数事件 2 function removeEvent(element, type, handler) { 3 // delete the event handler from the hash table 4 // 循环element.events[type],根据handler的唯一的id,进行delete 5 if (element.events && element.events[type]) { 6 delete element.events[type][handler.$$guid]; 7 } 8 };
移除函数比较简单,不多解释。
4. 总结
这段代码相对于jquery的事件系统,少了事件的代理,以及模拟的时间冒泡。不考虑代理,当然就简单许多。
但是它已经点出了jquery事件系统的原型,理解它,能帮助你更好的理解jquery事件系统。
补充:司徒正美的新书《javascript框架设计》中专门有一节讲解:11.4 Dean Edward的addEvent.js源码分析 260页