先看看下面一道题目,请评价以下代码并给出改进意见:
if (window.addEventListener) {//标准浏览器 var addListener = function(el, type, listener, useCapture) { el.addEventListener(type, listener, useCapture); }; } else if (document.all) {//IE addListener = function(el, type, listener) { el.attachEvent("on" + type, function() { listener.apply(el); }); } }
1)不应该在if和else语句中声明addListener函数,应该先声明;
2)不需要使用window.addEventListener或document.all来进行检测浏览器,应该使用能力检测;
3)attachEvent在IE中有this指向问题,会指向window,虽然上面的代码做了指向处理,但是匿名函数不能做detachEvent解绑
改进后的代码稍后加上。
一、冒泡与捕获
使用过addEventListener方法的会发现最后一个参数“useCapture”,用于控制是捕获还是冒泡。
何为冒泡、捕获,请看下面的例子:查看在线代码,在线代码中用了三层。
<div id="click1"> <div id="click2">事件</div> </div>
1)Netscape主张元素1的事件首先发生,这种事件发生顺序被称为捕获型,如下图所示:
| | ---------------| |----------------- | click1 | | | | -----------| |----------- | | |click2 \ / | | | ------------------------- | | Event CAPTURING | -----------------------------------
2)微软则保持元素2具有优先权,这种事件顺序被称为冒泡型,如下图所示:
/ \ ---------------| |----------------- | click1 | | | | -----------| |----------- | | |click2 | | | | | ------------------------- | | Event BUBBLING | -----------------------------------
3)W3C选择了一个择中的方案。任何发生在w3c事件模型中的事件,首是进入捕获阶段,直到达到目标元素,再进入冒泡阶段,如下图所示:
| | / \ ---------------| |--| |------------ | click1 | | | | | | -----------| |--| |------ | | |click2 \ / | | | | | ------------------------- | | W3C event model | -----------------------------------
4)阻止冒泡,很多时候是不想触发父级的相同事件的,那么就需要阻止这种行为。
W3C方:event.stopPropagation()。(chrome、firefox、safrai等)
IE方:event.cancelBubble设置为true。
经过我的在线测试,stopPropagation这个方法不能阻止捕获。
这里顺带说下阻止默认事件的方法,何为默认事件?就比如a标签设置了href,就会做跳转,这里阻止它跳转。
W3C方:event.preventDefault(),但只有event的cancelable属性为true时才能使用。
IE方:event.returnValue设置为false。
二、事件系统
浏览器提供了3种层次的API。
1)最原始的是写在元素标签内
2)以el.onXXX=function绑定的方式,通称为DOM0事件系统。
3)一个元素的同一类型事件可以绑定多个回调,通称为DOM2事件系统。
IE与W3C依旧不同,语法如下:
序号 | 操作与对象 | IE方 | W3C方 |
1 |
绑定事件 |
el.attachEvent("on"+type, callback) | el.addEventListener(type,callback,[useCapture]) |
2 |
卸载事件 |
el.detachEvent("on"+type,callback) | el.removeEventListener(type,callback,[useCapture]) |
3 | 创建事件 | document.createEventObject() |
document.createEvent(types) 创建事件(过时) event.initEvent() 初始化事件(过时) new Event(types) |
4 |
派发事件 |
el.fireEvent(type,event) | el.dispatchEvent(event) |
5 | event属性 | srcElement:等于target,默认目标 |
currentTarget:其事件处理程序当前正处理事件的元素 target:事件的目标 |
6 | event方法 |
returnValue:等于preventDefault() cancelBubble:设为true等于stopPropagation() |
preventDefault():阻止默认行为 stopPropagation():阻止冒泡 |
7 |
type |
被触发的事件类型,需要“on”前缀 | 被触发的事件类型 |
8 |
事件执行顺序 |
与添加顺序相反 | 与添加顺序一致 |
9 |
匿名函数 |
无法移除 | 无法移除 |
10 |
this |
window | 当前绑定的元素 |
注意第8点,在测试代码中,我绑定了两个相同的“click”事件, 查看在线完整代码。
var func1 = function(e) { alert(1); //测试执行顺序 }; var func2 = function(e) { alert(2); }; var type = 'click'; bind(ele, type, func1); bind(ele, type, func2);
在IE中先弹出2,再弹出1。而在chrome中先弹出1,再弹出2。
注意上面的第9点和第10点,上面那道题目中要解决的就是这个问题。下面的代码是个片段,查看在线完整代码。
var bind = function(ele, type, callback) { if (!ele[type + "event"]) { ele[type + "event"] = {}; //声明一个空对象,缓存事件 } var name = callback.toString(); if (!ele[type + "event"][name]) { var handler = function(event) { //可以做更多event封装操作 var ev = event || window.event; callback.call(ele, ev); }; ele[type + "event"][name] = handler; //做个临时变量 } if (ele.addEventListener) { ele.addEventListener(type, handler, false); } else if (ele.attachEvent) { ele.attachEvent('on' + type, handler); } } var unbind = function(ele, type, callback) { var handler = ele[type + "event"][callback.toString()]; //读取临时变量 if (ele.removeEventListener) { ele.removeEventListener(type, handler); } else if (ele.detachEvent) { ele.detachEvent('on' + type, handler); } }
上面的代码还比较粗糙,仅仅是用于演示一下。方法有很多,自己可以揣摩。
我看到网上有人直接不用attachEvent,将相应的事件保存在一个数组中,当detachEvent的时候做splice数组的操作。
大家也可以参考下一些成熟的类库,例如jQuery1.8.3版本,2642行的event.add封装,2757行的event.remove封装,3485行的on封装。
也可以参考Dean Wdwards写的addEvent方法,这是Prototype时代早期出现的一个事件系统,jQuery事件系统的源头。
参考资料: