第十三章
一、理解事件流
事件流描述的是从页面中接收事件的顺序。
1.事件冒泡
IE的事件流叫做事件冒泡,即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。以下面的HTML页面为例:
<!DOCTYPE html>
<html>
<head>
<title>Event Bubling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
如果你单机了页面中的<div>元素,那么这个click事件会按照如下顺序传播:
①<div>
②<body>
③<html>
④document
也就是说,click事件首先在<div>元素上发生,而这个元素就是我们单击的元素。然后click事件沿DOM树向上传播,在每一级节点上都会发生,直至传播到document对象。
2.事件捕获
另一种事件流叫做事件捕获。事件捕获的思想是不太具体的节点应该更早的接收到事件。如果仍以前面的页面作为演示事件捕获的例子,那么单击<div>元素就会以下列顺序触发click事件。
①document
②<html>
③<body>
④<div>
在事件捕获过程中,document对象首先接收到click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,即<div>元素。
3.DOM事件流
“DOM2级事件”规定的事件流包括三个阶段:事件的捕获阶段、处于目标阶段和事件冒泡阶段。
二、事件处理程序
1.HTML事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定。
在HTML中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本,如下面的例子所示:
<script type = “text/javascript”>
function showMessage(){
alert(“Hello world!”);
}
</script>
<input type = “button” value = “Click Me” onclick = “showMessage()” />
在这个例子中,单击按钮就会调用showMessage()函数。这个函数是在一个独立的<script>元素中定义的,当然代码也可以被包含在一个外部文件中,事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
这样指定事件处理程序有一些独到之处。首先会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象:
<!—输出“click”-->
<input type=”button” value=”click Me” onclick =”alert(event.type)”>
通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this值等于事件的目标元素,例如:
<!—输出”Click Me”-->
<input type=”button” value=”Click Me” onclick=”alert(this.value)”>
关于这个动态创建函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。
2.DOM0级事件处理程序
通过javascript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。
每个元素,都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:
var btn = document.getElementById(“myBtn”) ;
btn.onclick = function() {
alert(“Clicked”) ;
} ;
在此,我们通过文档对象取得了一个按钮的引用,然后为它指定了onclick事件处理程序。使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素。来看一个例子:
var btn = document.getElementById(“myBtn”) ;
btn.onclick = function() {
alert(this.id) ; //”myBtn”
} ;
单击按钮显示的是元素的ID,这个ID是通过this.id取得的。
也可以通过删除DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为null即可:
btn.onclick = null ; //删除事件处理程序
3.DOM2级事件处理程序
“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序和函数的一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。
使用DMO2级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.addEventListener("click",function(){
alert(this.id);
},false);
btn.addEventListener("click",function(){
alert("Hello world!");
},false);
通过addEventListener() 添加的事件处理程序只能用removeEventListener()来移除;移除同时传入的参数与添加处理程序时使用的参数相同。这意味着通过addEventListener() 添加的匿名函数将无法移除。
4.IE事件处理程序
IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接收相同的2个参数:事件处理程序名称好事件处理程序函数。与DOM方法不同的是:
(1)DOM0级方法事件处理程序会在其所属元素的作用域中运行,而attachEvent()方法在全局作用域中运行。
(2)attachEvent()事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。
5.跨浏览器的事件处理程序
第一个要创建的方法是addHandler(),它的职责是视情况分别使用DOM0级方法、DOM2级方法或IE方法来添加事件。这个方法属于一个名叫EventUtil的对象,它接受3个参数:要操作的元素、事件名称和事件处理程序函数。
与addHandler()对应的方法是removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序——无论该事件处理程序是采取什么方式添加到元素中的,如果其他方法无效,默认采用DOM0级方法。
三、事件对象
1.DOM中的事件对象
兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法,都会传入event对象。来看下面的例子:
var btn = document.getElementById(“myBtn”) ;
btn.onclick = function(event){
alert(”event.type”);
};
btn.addEventListener(“click” ,function(event){
alert(event.type); //“click”
},false);
这个例子中的两个事件处理程序都会弹出一个警告框,显示由event.type属性表示的事件类型。
2.IE中的时间对象
与访问DOM中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。来看下面的例子:
var btn = document.getElementById(“myBtn”) ;
btn.onclick = function() {
var event = window.event ;
alert(event.type) ; //”click”
};
通过window.event取得了event对象,并检测了被触发事件的类型。
3.跨浏览器的事件对象
虽然DOM和IE中的event对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。IE中event对象的全部信息和方法DOM对象中都有,只不过实现方式不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。
4.事件类型
(1)UI事件,当用户与页面上的元素交互时触发;
(2)焦点事件,当元素获得或是去焦点时触发;
(3)鼠标事件,当用户通过鼠标在页面上执行操作时触发;
(4)滚轮事件,当使用鼠标滚轮时触发;
(5)文本事件,当在文档中输入文本时触发;
(6)键盘事件,当用户通过键盘在页面上执行操作时触发;
(7)合成事件,当为IEM输入字符时触发;
(8)变动事件,当底层DOM结构发生变化时触发。
(9)变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们。
五、性能和内存
1.事件委托
对“事件处理程序过多”问题的解决方案就是事件委托。事件委托利用了事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。例如,click事件会一直冒泡到document层次。也就是说,我们可以为整个页面指定一个onclick事件处理程序,而不必给每个可单击的元素分别添加事件处理程序。以下面的HTML代码为例:
<ul id=”myLinks”>
<li id=”goSomewhere”>Go somewhere</li>
<li id=”doSomething”>Do something</li>
<li id=”sayHi”>Say hi</li>
</ul>
其中包含3个被单击后会执行操作的列表项。按照传统做法,需要像下面这样为它们添加3个事件处理程序。
var item1=document.getElementById("goSomewhere");
var item2=document.getElementById("doSomething");
var item3=document.getElementById("sayHi");
EventUtil.addHandler(item1,"click",function(event){
location.href="http://www.wrox.com";
});
EventUtil.addHandler(item2,"click",function(event){
document.title="I changed the document's title";
});
EventUtil.addHandler(item3,"click",function(event){
alert("hi");
});
如果在一个复杂的web应用程序中,对所有可单击的元素都采用这种方式,那么结果就会有数不清的代码用于添加事件处理程序。此时,可以利用事件委托技术解决这个问题。使用事件委托,只需在DOM树中尽量最高的层次上添加一个事件处理程序,如下面的例子。
var list=document.getElementById("myLinks");
EventUtil.addHandler(list,"click",function(event){
event=EventUtil.getEvent(event);
var target=EventUtil.getTarget(event);
switch(target.id){
case "doSomething";
document.title="I changed the document's title";
break;
case "goSomewhere";
location.href="http://www.wrox.com";
break;
case "sayHi";
alert("hi");
break;
}
});
2.移除事件处理程序
每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的javascript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的链接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”,也是造成Web应用程序内存与性能问题的主要原因。
在两种情况下,可能会造成上述问题:第一种情况就是从文档中移除带有事件处理程序的元素时。第二种情况就是卸载页面的时候。
六、模拟事件
1.DOM中的事件模拟
可以在document对象上使用createEvent()方法创建event对象。这个方法接收一个参数,即表示要创建的事件类型的字符串。在DOM2级中所有这些字符串都是使用英文复数形式,而在DOM3级中变成了单数。
(1)模拟鼠标事件
创建新的鼠标事件对象并为其指定必要的信息就可以模拟鼠标事件。创建鼠标事件对象的方法是为createEvent()传入字符串“MouseEvents”。返回的对象有一个名为initMouseEvent()方法,用于指定与该鼠标事件有关的信息。这个方法接收15个参数。下面, 我们通过一个例子了解如何模拟对按钮的单击事件:
var btn=document.getElementById(“myBtn”);
// 创建事件对象
var event = document.createEvent(“MouseEvents”);
//初始化事件对象
event.initMouseEvent(“click”,true,true,document.defaultView,0,0,0,0,0,false,false,false,false,0,null);
//触发事件
btn.dispatchEvent(event);
(2)模拟键盘事件
DOM3级规定,调用createEvent()并传入“keyboardEvent”就可以创建一个键盘事件。返回的事件对象会包含一个initKeyEvent()方法。
由于DOM3级不倡导使用keypress事件,因此只能利用这种技术来模拟keydown和keyup事件。
var textbox=document.getElementyById(“myTextbox”),event;
//以DOM3级方式创建事件对象
if (document.implementation.hasFeature(“KeyboardEvents”,”3.0”)){
event=document.createEvent(“KeyboardEvent”);
//初始化事件对象
event.initKeyboardEvent(“keydown”,true,true,document.defaultView,”a”,0,”shift”,0);
}
//触发事件
textbox.dispatchEvent(event);
这个例子模拟的是按住shift键的同时又按下A键。
在其他浏览器中,则需要创建一个通用的事件,然后再向事件对象中添加键盘事件特有的信息。例如:
var textbox=document.getElementById(“myTextbox”);
//创建事件对象
var event=document.createEvent(“Events”);
//初始化事件对象
event.initEven(type,bubbles,cancelable);
event.view=document.defaultView’
event.altKey=false;
event.ctrlKey=false;
event.shiftKey=false;
event.metaKey=false;
event.keyCode=65;
event.charCode=65;
//触发事件
textbox.dispatchEvent(event);
3.模拟其他事件
虽然鼠标事件和键盘事件是在浏览器中最经常模拟的事件,但有时候同样需要模拟变动事件和HTML事件。要模拟变动事件,可以使用createEvent(“MutationEvents”)创建一个包含initMutationEvent()方法的变动事件对象。例如:
var event=document.createEven(“MutationEvents”) ;
event.initMutationEvent(“DOMNodeInserted”,true,false,someNode,””,””,””,0);
target.dispatchEvent(event);
以上代码模拟了DOMNodeInserted事件。其他变动事件可以按照这个样子来模拟,只要改一改参数。
4.自定义DOM事件
自定义事件不是由DOM原生触发的,它的目的是让开发人员创建自己的事件。要创建新的定义事件,可以调用createEvent(“CustomEvent”)。返回的对象有一个名为initCustomEvent()的方法。
2.IE中的事件模拟
调用document.createEventObject()方法可以在IE中创建event对象。这个方法不接受参数,结果会返回一个通用的event对象。然后,手工为这个对象添加所有必要的信息。最后一步就是在目标上调用fireEvent()方法,这个方法接受两个参数:事件处理程序的名称和event对象。在调用fireEvent()方法时,会自动为event对象添加srcElement和type属性;其他属性则都必须通过手工添加。