前言: JAVASCRIPT与HTML之间的交互是通过事件来实现的。事件,就是文档或浏览器窗口中发生的一些特定交互瞬间。可以使用侦听器( 或处理程序 )来预定事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式的模型,支持页面的行为与页面的外观之间的松散耦合。
事件最早是在IE3和Netscape Navigator 2中出现的,当时是作为分担服务器运算负载的一种手段。在IE4和Navigator 4发布时,这两种浏览器都提供了相似但是不同的API, 这些API并存经历了好几个主要版本。DOM2 级规范开始尝试以一种符合逻辑的方式来标准化DOM事件。IE9, Firefox,Opera,Safari和Chrome全都已经实现了“DOM2级事件”模块的核心部分。IE8是最后一个仍然使用专用事件系统的主要浏览器。
<一> 事件流
事件流描述的是从页面中接受事件的顺序。但有意识的是,IE和Navigator 开发团队居然提出了差不多是完全相反的事件流概念。IE的事件流是冒泡流, 而Navigator 的事件流是捕获流。
1.1 事件冒泡
IE的事件流叫做事件冒泡(event bubbling), 即事件开始时由最具体的元素( 文档中嵌套层次最深的那个节点 )接受,然后逐级向上传播到较为不具体的节点(文档)。
举例click事件
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <div onclick="test()">事件</div> </body> <script type="text/javascript"> function test() { alert('test event bubbling') } </script> </html>
click事件传播顺序: div --> body --> html --> document
click事件首先在<div>元素上发生, 而这个元素就是我们单击的元素。然后,click事件沿着DOM树向上传播,在每一个节点上都发生,直到传播到document对象。
备注: 所有现代浏览器都支持事件冒泡,但是在具体实现上还是有一些差别。IE5.5 及更早版本中的事件冒泡会跳过<html>元素 (从<body>直接跳转到<document>)。IE9,Firefox,Chrome和Safari则将事件一直冒泡到window对象。
1.2 事件捕获
事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于事件到达预定目标之前捕获它。
虽然事件捕获是Navigator Communcator唯一支持的事件流模型,但是IE9,Safari,Chrome,Opera和Firefox目前也都支持这种事件流模型。
1.3 DOM事件流
DOM2级事件: 规定的事件流包括三个阶段: 事件捕获阶段,处于目标阶段和事件冒泡阶段。
首先发生的是事件捕获,为截取事件提供了机会。然后是实际目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件作出响应。具体流经如下图所示。
<二> 事件处理程序
事件就是用户或浏览器自身执行的某种动作。诸如click,load和mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦听器)。事件处理程序的名字以‘on’,因此click事件的事件处理程序就是onclick。为事件指定处理程序的方式有好几种
2.1 HTML事件处理程序
直接绑定在元素上,或者引入函数名
<body> <input type="button" onclick="alert('test')" value="按钮"> <div onclick="test()">事件</div> </body> <script type="text/javascript"> function test() { alert('test event bubbling') } </script>
2.2 DOM0级事件处理程序
通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值方法是在第四代web浏览器中出现的,而且至今仍然为所有浏览器所支持。原因一:简单;原因二:具有跨浏览器的优势。要使用JavaScript指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素(包括window和document)都有自己的事件处理程序,这些属性通常全部小写,例如
<body> <div id="btn">提示</div> </body> <script type="text/javascript"> var btn = document.getElementById('btn'); btn.onclick = function () { alert('事件绑定成功') } </script>
如果要解除事件绑定
btn.onclick = null;
2.3.1 DOM2级事件处理程序
"DOM2级事件" 定义了两个方法,用于处理指定事件和删除事件处理程序的操作,addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且他们都接受3个参数:要处理事件名,作为事件处理程序的函数和一个布尔值。 最后这个参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调动事件处理程序。
可以为一个元素添加多个事件处理程序,事件处理程序会按照它们添加的顺序依次执行。
<body> <div id="btn">提示</div> </body> <script type="text/javascript"> var btn = document.getElementById('btn'); btn.addEventListener('click',function () { console.log('第一个事件'); },false); btn.addEventListener('click',function () { console.log('hello world'); },false); </script>
2.3.2 IE事件处理程序
IE实现了与DOM中类型的两个方法:attachEvent()和detachEvent()。这两个方法都接受相同的参数:事件处理程序名称,事件处理程序函数。由于IE8及更早版本只支持冒泡事件,所以通过attachEvent()添加的事件出现程序都会被添加到冒泡阶段
在IE中使用attachEvent()与DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域下运行,因此this等于window。
<body> <div id="btn">提示IE</div> </body> <script type="text/javascript"> var btn = document.getElementById('btn'); btn.attachEvent('onclick',function () { console.log(this); console.log('第一个事件'); }); btn.attachEvent('onclick',function () { console.log('hello world'); }); </script>
注意:
1. IE事件处理程序不是以添加它们的顺序执行,而是以相反的顺序执行;
2. 绑定事件时,IE要添加 ‘on’。
2.3.3 移除事件处理程序
要移除事件处理程序,要传入函数名。不能移除匿名函数,移除匿名函数时无效的。
<body> <div id="btn">提示</div> </body> <script type="text/javascript"> var btn = document.getElementById('btn'); var handler = function () { console.log('第一个事件'); }; btn.addEventListener('click',handler,false); btn.removeEventListener('click',handler,false); </script>
2.3.4 总是所述,跨浏览器的事件处理程序。
var EventUtil = {
addHandler:function (element, type, handler) {
if(element.addEventListener){
element.addEventListener(type,handler,false)
}else if(element.attachEvent){
element.attachEvent('on'+type,handler)
}else {
element['on'+type] = handler;
}
},
rmoveHandler:function (element, type, handler) {
if(element.removeEventListener){
element.removeEventListener(type,handler,false);
}else if(element.detachEvent){
element.detachEvent('on'+type,handler);
}else {
element['on'+type] = null;
}
},
};
<三> 事件对象
3.1 在出发DOM上的某个事件时,会产生一个事件对象event, 这个对象种包含着所有与事件有关的信息。包括导致事件的元素,事件的类型,以及其他与特定事件相关的信息。
附件:所有event的详细信息: W3C_HTML DOM Event 对象
<四> 事件类型
4.1 判断是否支持DOM2,DOM3事件
document.implementation.hasFeature("HTMLEvent","2.0");
document.implementation.hasFeature("UIEvent","3.0");
备注: 只有根据“DOM2/3级事件”实现这些事件(load,unload,abort,resoze,scroll......)的浏览器才会返回true。而以非标准方式支持这些事件的浏览器则返回false。
4.2 鼠标和滚轮事件
4.2.1 事件说明
click:在用户单机主鼠标按钮(一般是左边的按钮)或者按下回车键时出发。这一点对确保易访问性很重要,意味着onclick事件处理程序即可以通过键盘也可以通过鼠标执行。
dbclick: 在用户双击主鼠标(一般是左边的按钮)时触发。从技术上说,这个事件并不是DOM2级事件规范种规定的,但鉴于它得到了广泛的支持,所以DOM3级事件将其纳入了规范。
mousedown: 在用户按下了任意鼠标时触发。不能通过键盘触发该事件。
mouseenter: 在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。IE9,Firefox9+和Opera支持这个事件。
mouseleave:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。IE9,Firefox9+和Opera支持这个事件。
mousemove:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发该事件。
mouseout:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发该事件。
mouseover:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发该事件。
mouseup:在用户释放鼠标按钮指针时触发。不能通过键盘触发该事件。
备注: 只有在同一个元素上触发mousedown和mouseup事件,才会触发cilick事件;如果mousedown或mouseup中的一个被取消,就不触发click事件。
类似地,只有触发两次click事件,才会触发一次dbclick,如果代码组织了连续两次click事件(可能是直接取消click,也可能通过取消mousedown或mouseup间接实现),那么就不会触发dbclick。这四个事件触发的顺序始终如下:
1. mousedown 2. mouseup 3. click 4. mousedown 5. mouseup 6. click 7. dbclick
click和dbclick事件都依赖于其他事件先行触发,而mousedown和mouseup则不受其他影响。
IE8及之前版本中的实现有个bug,触发的事件顺序如下:
1. mousedown 2. mouseup 3. click 4. mouseup 5. dbclick
4.2.2 修改键
虽然鼠标事件主要是使用鼠标来触发,但是在按下鼠标时键盘上的某些键盘的状态也可以影响到所要采取的操作。这些修改键就是Shift,Ctrl,Alt和Meta(苹果Cmd键),它们经常被用来修改鼠标事件的行为。DOM为此规定了4个属性,表示这些修改键的状态:shiftKey,ctrlKey,altKey和MetaKey。这些属性中包含的都是布尔值,如果相应的键被按下了,则值为true,否则为false。当某个鼠标事件发生时,通过检测这几个属性就可以确定用户是否同时按下了其中的键。示例如下:
<body> <div id="btn">提示</div> </body> <script src="./js/base.js"></script> <script type="text/javascript"> var btn = document.getElementById('btn'); EventUtil.addHandler(btn,'click',function (event) { event = EventUtil.getEvent(event); var keys = []; if(event.shiftKey){ keys.push('shift'); } if(event.ctrlKey){ keys.push('ctrl') } if(event.altKey){ keys.push('alt') } if(event.metaKey){ keys.push('meta') } }) </script>
4.2.3 鼠标按钮
只有在主鼠标按钮被单击(或键盘回车键被按下)时才会触发click事件,因此检测按钮的信息并不是必要的。但对于mousedown和mouseup事件来说,则在其event对象存在一个button属性,表示按下或释放的按钮。
DOM的button属性可能有如下3个值:0表示主鼠标按钮,1表示中间的鼠标按钮(鼠标滚轮按钮),2 表示次鼠标按钮。 在常规的设置中,主鼠标按钮就是鼠标左键,而次鼠标按就是鼠标右键。
IE8及之前版本也提供了button属性,但是这个属性值与DOM的button有很大差异。
0:表示没有按下按钮;
1:表示按下了主鼠标按钮;
2:表示按下了次鼠标按钮;
3:表示同时按下了主鼠标按钮,次鼠标按钮;
4:表示按下了中间鼠标按钮;
5:表示同时按下了主鼠标按钮和中间鼠标按钮;
6:表示同时按下了次鼠标按钮和中间鼠标按钮;
7:表示同时按下了三个鼠标按钮。
getButton:function (event) { if(document.implementation.hasFeature("MouseEvent","2.0")){ return event.button; }else { switch (event.button){ case 0: case 1: case 3: case 5: case 7: return 0; case 2: case 6: return 2; case 4: return 1; } } },
4.2.4 相关元素
DOM通过event对象的releateTarget属性提供了相关元素信息。这个属性只对于mouseover和mouseout事件才包含值,对于其他事件,这个属性的值是null。
IE8及之前版本不支持releateTarget属性,但提供了保存同样信息的不同属性。在mouseover事件触发时,IE的fromElement属性中保存了相关元素;在mouseout事件触发时,IE的toElement属性中保存了相关元素。
IE9及以后IE浏览器支持所有这些属性。
getRelateTarget:function (event) { if(event.relatedTarget){ return event.relatedTarget; }else if(event.toElement){ return event.toElement; }else if(event.fromElement){ return event.fromElement; }else { return null; } },
<五> 综上所述,总结归纳了通用的事件侦听器函数总体封装,以供以后学习和使用
var EventUtil = { addHandler:function (element, type, handler) { if(element.addEventListener){ element.addEventListener(type,handler,false) }else if(element.attachEvent){ element.attachEvent('on'+type,handler) }else { element['on'+type] = handler; } }, rmoveHandler:function (element, type, handler) { if(element.removeEventListener){ element.removeEventListener(type,handler,false); }else if(element.detachEvent){ element.detachEvent('on'+type,handler); }else { element['on'+type] = null; } }, getEvent:function (event) { return event ? event : window.event; }, getTarget:function (event) { return event.target || event.srcElement; }, preventDefault:function (event) { if(event.preventDefault){ event.preventDefault(); }else{ event.returnValue = false; } }, stopPropagation:function (event) { if(event.stopPropagation){ event.stopPropagation(); }else{ event.cancelBubble = true; } }, getRelateTarget:function (event) { if(event.relatedTarget){ return event.relatedTarget; }else if(event.toElement){ return event.toElement; }else if(event.fromElement){ return event.fromElement; }else { return null; } }, getButton:function (event) { if(document.implementation.hasFeature("MouseEvent","2.0")){ return event.button; }else { switch (event.button){ case 0: case 1: case 3: case 5: case 7: return 0; case 2: case 6: return 2; case 4: return 1; } } }, getCharCode:function (event) { if(typeof event.charCode === 'number'){ return event.charCode; }else { return event.keyCode; } } };
<六> 事件委托
推荐参考学习地址: js中的事件委托或是事件代理详解
由于事件处理程序可以为现代web应用程序提供交互能力,因此许多开发人员会不分青红皂白地向页面中添加大量地处理程序. 在创建GUI地语言(C#)中, 为GUI中地每个按钮添加一个onclick事件处理程序是司空见惯地事情,而且这样做也不会导致什么问题. 可是在JavaScript中, 添加到页面上的事件处理程序数量将直接关系到页面到整体性能. 导致这一问题到原因很多. 首先,每个函数都是对象, 都会占用内存, 内存中的对象越多性能就越差. 其次, 必须事先指定所有事件处理程序而导致的DOM访问次数, 会延迟整个页面的交互就绪时间. 事实上, 从如何利用好事件处理程序的角度触发, 还是有一些方法能够提升性能.
示例扩展:
<body> <ul id="test"> <li style="height: 56px;background-color: red"> <p style="background-color: #1F89bf;height: 30px;">11111111111</p> </li> <li> <div> <p> <div> <span> <span> <span> <span>99999</span> </span> </span> </span> </div> </p> </div> </li> </ul> </body> <script type="text/javascript"> window.onload = function () { var oUl = document.getElementById('test'); oUl.addEventListener('click',function(event){ var target = EventUtil.getEvent(event); var i = 0; while(target !== oUl ){ i++; if(target.tagName.toLowerCase() === 'li'){ // 处理相应的函数... break; } target = target.parentNode; } console.log(i,'num'); // 7 "num" }) } </script>