Html页面与JavaScript之间的交互是通过事件来完成的。事件,就是文档或者浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(处理程序)来预订事件,以便事件发生时执行相应的代码。这在传统的软件工程中称为观察者模式。
事件流描述的是从页面中接收事件的顺序。但是IE团队和Netscape团队提出了完全相反的事件流概念。IE提出的是事件冒泡流,Netscape提出的是事件捕获流。下面我们就来详细介绍这方面的内容。
IE的事件流是事件冒泡。即事件开始时由最具体的元素(文档中嵌套层次最深的那个节点)接收,然后逐级向上传播到较不具体的节点,直至最后传播到文档。请看下面的例子:
1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 5 <title></title> 6 </head> 7 <body> 8 <div id="myDiv">Click Me</div> 9 </body> 10 </html>
如果你点击了页面中的DIV元素。那么click事件会按照如下的顺序进行传播。
1、<div>
2、body
3、html
4、document
click事件会首先在<div>元素上触发,这个元素就是我们点击的元素。然后,click事件沿着DOM树向上传递,在每一级的节点上都会触发,直至到document对象。下面这张图很好的展示了这个过程。
事件捕获的思想正好和事件冒泡相反,是不太具体的元素首先接收到事件,而更具体的节点在最后接收到事件。还是上面的HTML代码。单击DIV元素会按照以下的顺序触发click事件。
1、document
2、html
3、body
4、<div>
如图所示:
DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是事件冒泡,可以在这个阶段对事件作出反应。我们还是以前面的文档为例,单击DIV元素会按照如下图片所示的顺序触发事件。
多数支持DOM事件流的浏览器都实现了一种特定的行为:即使"DOM2级事件"明确规定捕获阶段不会涉及目标事件,但IE9、Safari、Chrome、Firefox及Opera9.5以上的浏览器都会在捕获阶段触发事件对象上的事件。注意:IE8及低版本浏览器不支持DOM事件流。
HTML事件处理程序是最基本的通过JavaScript与浏览器交互的技术。请看下面的例子:
1 <!DOCTYPE html> 2 <html xmlns="http://www.w3.org/1999/xhtml"> 3 <head> 4 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 5 <title></title> 6 </head> 7 <body> 8 <input id="myDiv" value="Click Me" type="button" onclick="showMessage()"/> 9 </body> 10 </html> 11 <script type="text/javascript"> 12 function showMessage() { 13 alert("Hello World!"); 14 } 15 </script>
以上代码造成HTML与JavaScript代码紧密耦合。如果要更改事件处理程序,就需要改动两个地方,HTML页面与JavaScript代码。下面介绍DOM0级事件。这在一定程度上可以缓解代码耦合问题。
通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理函数的属性。这种方式有两个优势:1、简单。2、跨浏览器支持。
每一个元素(包括document)都有自己的事件处理函数属性。这些属性通常都是小写的。例如:onclick等。请看下面的例子:
1 <script type="text/javascript"> 2 var btn = document.getElementById("btn"); 3 btn.onclick = function () { 4 alert("Click Button!"); 5 } 6 </script>
使用这种方式有一点需要注意:在这些代码运行以前,不会指定事件处理程序。使用DOM0级事件处理程序被认为是元素的方法。因此,这个时候事件处理程序是在元素的作用域中运行的。程序中的this指向当前元素。同时将btn.onclick=null可以删除事件处理程序。
DOM2级事件处理程序指定了两个方法用于指定和删除事件处理程序的操作。分别是addEventListener和removeEventListener。所有DOM节点都包含这两个方法。并且它们有3个参数:要处理的事件名、作为事件处理程序的函数及一个布尔值。最后布尔值如果是true表示在捕获阶段触发调用事件处理程序。否则表示在冒泡阶段触发事件处理程序。
请看下面的例子:
1 <script type="text/javascript"> 2 var btn = document.getElementById("btn"); 3 /* 4 btn.onclick = function () { 5 alert("Click Button!"); 6 } 7 */ 8 9 btn.addEventListener("click", function () { 10 alert("Click Button!"); 11 }, false); 12 </script>
大多数情况下,都是将事件处理程序添加到事件冒泡阶段来处理,这样可以最大先丢的兼容这个版本的浏览器。一般不怎么建议将事件处理程序注册在事件捕获阶段。
IE中实现了与DOM中类似的两个方法。attachEvent和detachEvent。这两个方法接收相同的两个参数。事件名称和事件处理程序函数。IE8及之前的版本只支持事件冒泡,所以通过attachEvent添加的事件处理程序都会添加到冒泡阶段。
请看下面的例子:
1 btn.attachEvent("onclick", function () { 2 alert("Click Button!"); 3 });
在IE中使用attachEvent与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级的情况下,事件处理程序会在其所属元素的作用域中运行。使用attachEvent方法的情况下,事件处理程序会在全局作用域中运行。
由于现在IE8及其版本的浏览器还是占了较大的比重,所以有必要使用跨浏览器的方案来进行事件处理程序的处理。一般我们都使用JQuery等JS库来隔离这种差异。当然自己写也可以。适当的使用能力监测即可。请看下面的例子:
1 /** 2 * 提供跨浏览器的事件处理对象 3 **/ 4 EventModule = { 5 /** 6 * 为HTML页面中的元素添加指定的事件 7 * element:需要添加事件处理函数的标签 8 * type:添加的事件类型如:"click","focus"等 9 * handler:事件处理函数 10 **/ 11 addHandler: function (element, type, handler) { 12 //使用能力监测,看浏览器是否支持DOM2级事件 13 if (element.addEventListener) { 14 element.addEventListener(type, handler, false); 15 } 16 //如果支持IE的事件模型,就使用IE的事件模型 17 else if (element.attachEvent) { 18 element.attachEvent("on" + type, handler); 19 } 20 //最后使用DOM0级事件 21 else { 22 element["on" + type] = handler; 23 } 24 }, 25 26 /** 27 * 为HTML页面中的元素移除指定的事件 28 * element:需要添加事件处理函数的标签 29 * type:添加的事件类型如:"click","focus"等 30 * handler:事件处理函数 31 **/ 32 removeHandler: function (element, type, handler) { 33 if (element.removeEventListener) { 34 element.removeEventListener(type, handler, false); 35 } 36 else if (element.detachEvent) { 37 element.detachEvent("on" + type, handler); 38 } 39 else { 40 element["on" + type] = null; 41 } 42 }, 43 }
我们看到我们封装了一个addHandler方法。这个方法隔离了各个浏览器在添加事件处理程序的差异。我们看到这里面首先使用能力检测,看浏览器是否支持DOM2级事件。否则的话检测是否支持IE的事件模型。最后使用DOM0级事件处理程序。这样就保证了可以兼容所有的浏览器。
在触发DOM上的某个事件时,出产生一个事件对象event,这个对象包含着所有与事件有关的信息,包括事件的目标元素、事件的类型及其他与特定事件相关的信息。
兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或者DOM2级)。event对象包含与创建它的特定事件有关的属性和方法。不过,下面列表展示的是所有事件都支持的属性和方法。
在这些属性中我先介绍currentTarget属性和target属性。在事件处理程序内部,this的值始终等于currentTarget。而target则只包含事件的实际目标。如果直接将事件处理程序指定给目标元素,则this、currentTarget、target的值是一样的。如果事件处理程序存在于按钮的父节点中(例如:document.body)。那么这些值是不相等的。请看下面的例子:
1 document.body.onclick = function (event) { 2 alert(document.body === event.currentTarget); //true 3 alert(this === document.body); //true 4 alert(event.target === document.getElementById("btn")); //true 5 }
当单击这个例子中的按钮时,this和currentTaget都等于document.body。因为事件处理程序是注册到这个元素上的。然而,target等于却等于按钮元素,因为它是click事件的真正的目标。由于按钮上没有注册事件处理程序,所有事件冒泡到document.body。并且在那里得到了处理。
要阻止特定事件的默认行为,我们可以使用preventDefault()方法。调用event.preventDefault()即可。(只有cancelable属性为true的事件才能使用该方法哦)
注意:只有在事件处理程序执行期间,event对象才会存在。一旦事件处理程序执行完毕,event对象就销毁了。
与访问DOM中的event对象不同,IE中的event对象可以通过以下几种方式进行访问。
1、使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。
2、事件处理函数是通过attachEvent方法添加的,那么event对象作为参数被传递到事件处理函数中。
下面这张表格是IE中所有事件都支持的属性和方法。对照着DOM事件对象的表格看,效果会更好。
前面我们看到无论是DOM事件对象还是IE特有的事件对象,都支持一些我们常用的功能。我们在编写跨浏览器时,完全可以将这两种事件对象结合起来。就像前面添加事件处理程序一样。下面,咱们就来简单的看下封装的跨浏览器的事件对象。
1 /** 2 * 获取对event对象的引用 3 **/ 4 getEvent: function (event) { 5 //考虑到IE浏览器中event对象位置的不同,使用window.event来兼容IE(IE中event是undefined) 6 return event ? event : window.event; 7 }, 8 9 /** 10 * 获取事件的目标 11 **/ 12 getTarget: function (event) { 13 return event.target || event.srcElement; 14 }, 15 16 /** 17 * 取消事件的默认行为 18 **/ 19 preventDefault: function (event) { 20 if (event.preventDefault) { 21 event.preventDefault(); 22 } 23 else { 24 //IE浏览器中returnValue=false就是取消事件的默认行为 25 event.returnValue = false; 26 } 27 }, 28 29 /** 30 * 停止事件的冒泡 31 */ 32 stopPropagation: function (event) { 33 if (event.stopPropagation) { 34 event.stopPropagation(); 35 } 36 else { 37 //IE浏览器中不支持事件捕获,通过设置cancleBubble可以停止事件的冒泡 38 event.cancleBubble = true; 39 } 40 }
由于事件处理程序可以为现代WEB程序提供交互能力,所以许多开发人员会在页面中添加大量的事件处理程序。但这样会导致严重的性能问题。具体表现在以下两个方面。
1、每一个函数都是对象,对象都会占用内存。事件处理程序过多,会导致内存占用多大,影响性能。
2、必须事先指定所有的事件处理程序导致DOM访问次数过多。会延迟整个页面的交互就绪时间。
事实上,通过一些简单的技术,我们可以在一定程度上缓解这种问题。事件委托就是很好的一个例子。还记得事件冒泡吗?我们使用的就是事件冒泡的原理。只指定一个事件处理程序,就可以处理某一类型的所有事件。例如:click事件会冒泡到document层次,我们可以为整个页面指定一个事件处理程序。而不必给某一个可单击的元素分别添加事件处理程序。请看下面的例子:
1 <ul id="myLinks"> 2 <li id="goSomeWhere">goSomeWhere</li> 3 <li id="doSomeThing">doSomeThing</li> 4 <li id="sayHay">sayHay</li> 5 </ul>
我们来看传统的添加事件处理程序的方式是:
1 var item1 = document.getElementById("goSomeWhere"); 2 var item2 = document.getElementById("doSomeThing"); 3 var item3 = document.getElementById("sayHay"); 4 5 EventModule.addHandler(item1, "click", function (event) { 6 //事件处理程序 7 }); 8 EventModule.addHandler(item2, "click", function (event) { 9 //事件处理程序 10 }); 11 EventModule.addHandler(item2, "click", function (event) { 12 //事件处理程序 13 });
如果一个页面中有着复杂的逻辑,肯定会有许多事件处理程序。这给页面的维护带来很大的问题。同时,这样写也会导致内存中存在大量的函数对象,影响页面的加载时间。我们可以通过事件委托来改进。请看改进后的例子:
1 var list = document.getElementById("myLinks"); 2 EventModule.addHandler(list, "click", function (event) { 3 event = EventModule.getEvent(event); 4 //获取事件的目标元素 5 var target = EventModule.getTarget(event); 6 7 switch (target.id) { 8 case "goSomeWhere": 9 { 10 //对应的事件处理程序 11 break; 12 } 13 case "doSomeThing": 14 { 15 //对应的事件处理程序 16 break; 17 } 18 case "sayHay": 19 { 20 //对应的事件处理程序 21 break; 22 } 23 } 24 });
在这段代码中,我们获取UL元素,并且给它添加事件处理函数,因为它的子节点事件会冒泡到父节点这一级。我们通过前面的学习也知道event对象封装了事件相关的信息。通过target.Id我们获取到事件的目标元素,然后添加对应的事件处理函数。通过这种方式我们不仅减少了DOM的操作。同时显著减少了事件处理函数的数量,节约了内存。
下面我会把自定义的事件处理模块EventModule添加在文档的后面。大家有兴趣可以一起看看,学习,谢谢大家。
EventModule源代码:
1 /** 2 * 提供跨浏览器的事件处理对象 3 **/ 4 EventModule = { 5 /** 6 * 为HTML页面中的元素添加指定的事件 7 * element:需要添加事件处理函数的标签 8 * type:添加的事件类型如:"click","focus"等 9 * handler:事件处理函数 10 **/ 11 addHandler: function (element, type, handler) { 12 //此处使用能力监测,看浏览器是否支持DOM2级事件 13 if (element.addEventListener) { 14 element.addEventListener(type, handler, false); 15 } 16 //如果支持IE的事件模型,就使用IE的事件模型 17 else if (element.attachEvent) { 18 element.attachEvent("on" + type, handler); 19 } 20 //最后使用DOM0级事件 21 else { 22 element["on" + type] = handler; 23 } 24 }, 25 26 /** 27 * 为HTML页面中的元素移除指定的事件 28 * element:需要添加事件处理函数的标签 29 * type:添加的事件类型如:"click","focus"等 30 * handler:事件处理函数 31 **/ 32 removeHandler: function (element, type, handler) { 33 if (element.removeEventListener) { 34 element.removeEventListener(type, handler, false); 35 } 36 else if (element.detachEvent) { 37 element.detachEvent("on" + type, handler); 38 } 39 else { 40 element["on" + type] = null; 41 } 42 }, 43 44 /** 45 * 获取对event对象的引用 46 **/ 47 getEvent: function (event) { 48 //考虑到IE浏览器中event对象位置的不同,使用window.event来兼容IE(IE中event是undefined) 49 return event ? event : window.event; 50 }, 51 52 /** 53 * 获取事件的目标 54 **/ 55 getTarget: function (event) { 56 return event.target || event.srcElement; 57 }, 58 59 /** 60 * 取消事件的默认行为 61 **/ 62 preventDefault: function (event) { 63 if (event.preventDefault) { 64 event.preventDefault(); 65 } 66 else { 67 //IE浏览器中returnValue=false就是取消事件的默认行为 68 event.returnValue = false; 69 } 70 }, 71 72 /** 73 * 停止事件的冒泡 74 */ 75 stopPropagation: function (event) { 76 if (event.stopPropagation) { 77 event.stopPropagation(); 78 } 79 else { 80 //IE浏览器中不支持事件捕获,通过设置cancleBubble可以停止事件的冒泡 81 event.cancleBubble = true; 82 } 83 } 84 };