JavaScript
与 HTML
之间的交互是通过事件实现的。事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应的代码。这种在传统软件工程中被称为观察员模式的模型,支持页面的行为(JavaScript
代码)与页面的外观(HTML
和 CSS
代码)之间的松散耦合。
事件流
事件流描述的是从页面中接收事件的顺序。但有意思的是, IE
和 Netscape
开发团队居然提出了差不多是完全相反的事件流的概念。 IE
的事件流是事件冒泡流,而 Netscape Communicator
的事件流是事件捕获流。
-
IE
的事件流叫做事件冒泡(event bubbling
),即事件开始时由最具体的元素(文档中嵌套层次最深的那节点)接收,然后逐级向上传播到较为不具体的节点(文档)。
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
click
点击的顺序:div => body => html => document
- 事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
click
点击的顺序: document => html => body => div
DOM事件流
DOM2级事件
规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
IE9
、Opera
、Firefox
、Chrome
和Safari
都支持DOM
事件流;IE8
及更早版本不支持DOM
事件流。
事件处理程序
事件就是用户和浏览器自身执行的某种动作,事件处理程序就是响应某个事件的函数就叫做事件处理函数,事件处理程序以on
开头。click
事件的事件处理程序就是 onclick
,load
事件的事件处理程序就是onload
。
HTML 事件处理程序
<input type="button" value="Click Me" onclick="alert('Clicked')" />
当单击这个按钮时,就会显示一个警告框。这个操作是通过指定 onclick
特性并将一些 JavaScript
代码作为它的值来定义的。
Dom0 及事件处理程序
通过 JavaScript
指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web
浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。
每个元素(包括 window
和 document
)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick
。将这种属性的值设置为一个函数,就可以指定事件处理程序,如下所示:
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert("Clicked");
};
使用
DOM0
级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this
引用当前元素。来看一个例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert(this.id); //"myBtn"
};
以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
也可以删除通过
DOM0
级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可:
btn.onclick = null; //删除事件处理程序
将事件处理程序设置为null
之后,再单击按钮将不会有任何动作发生。
DOM2 级事件处理程序
DOM2 级事件
定义了两个方法,用于处理指定和删除事件处理程序的操作: addEventListener()
和removeEventListener()
。所有 DOM
节点中都包含这两个方法,并且它们都接受 3 个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。
最后这个布尔值参数如果是 true
,表示在捕获阶段调用事件处理程序;如果是false
,表示在冒泡阶段调用事件处理程序。
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的 ID
,其次会显示"Hello world!"
消息。
btn.addEventListener("click", function(){
alert("Hello world!");
}, false);
因为在 addEventListener()
和removeEventListener()
中使
用了相同的函数。可以考虑将事件处理函数单独抽取出来。
var btn = document.getElementById("myBtn");
var handler = function(){
alert(this.id);
};
btn.addEventListener("click", handler, false);
//这里省略了其他代码
btn.removeEventListener("click", handler, false); //有效!
IE9
、Firefox
、Safari
、Chrome
和Opera
支持DOM2
级事件处理程序。
IE事件处理程序
IE
实现了与DOM
中类似的两个方法: attachEvent()
和 detachEvent()
。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。
由于
IE8
及更早版本只支持事件冒泡,所以通过attachEvent()
添加的事件处理程序都会被添加到冒泡阶段。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
注意,
attachEvent()
的第一个参数是onclick
,而非DOM
的addEventListener()
方法中的click
。
在IE
中使用 attachEvent()
与使用DOM0
级方法的主要区别在于事件处理程序的作用域。在使用 DOM0
级方法的情况下,事件处理程序会在其所属元素的作用域内运行;
在使用
attachEvent()
方法的情况下,事件处理程序会在全局作用域中运行,因此this
等于window
!
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert(this === window); //true
});
不过,与
DOM
方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
btn.attachEvent("onclick", function(){
alert("Hello world!");
});
这里调用了两次 attachEvent()
,为同一个按钮添加了两个不同的事件处理程序。单击这个例子中的按钮,首先看到的是"Hello world!"
,然后才是"Clicked"
。
跨浏览器的事件处理程序
// EventUtil 的用法如下所示。
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;
}
},
removeHandler: 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;
}
}
};
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
//这里省略了其他代码
EventUtil.removeHandler(btn, "click", handler);
这两个方法首先都会检测传入的元素中是否存在DOM2
级方法。如果存在 DOM2
级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false
(表示冒泡阶段)。
如果存在的是IE
的方法,则采取第二种方案。注意,为了在 IE8
及更早版本中运行,此时的事件类型必须加上"on"
前缀。
最后一种可能就是使用DOM0
级方法(在现代浏览器中,应该不会执行这里的代码)。此时,我们使用的是方括号语法来将属性名指定为事件处理程序,或者将属性设置为 null。
事件对象
在触发 DOM
上的某个事件时,会产生一个事件对象event
,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件对象中,会包含鼠标位置的信息,而键盘操作导致的事件对象中,会包含与按下的键有关的信息。
DOM中的事件对象
兼容DOM
的浏览器会将一个 event
对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0 级
或 DOM2 级
),都会传入event
对象。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(event){
alert(event.type); //"click"
};
btn.addEventListener("click", function(event){
alert(event.type); //"click"
}, false);
属性/方法 | 类 型 | 读/写 | 说 明 |
---|---|---|---|
bubbles |
Boolean |
只读 | 表明事件是否冒泡 |
cancelable |
Boolean |
只读 | 表明是否可以取消事件的默认行为 |
currentTarget |
Element |
只读 | 其事件处理程序当前正在处理事件的那个元素 |
defaultPrevented |
Boolean |
只读 | 为true 表 示 已 经 调 用 了 preventDefault() (DOM3级 事件中新增) |
detail |
Integer |
只读 | 与事件相关的细节信息 |
eventPhase |
Integer |
只读 | 调用事件处理程序的阶段: 1 表示捕获阶段, 2 表示“处于目标”, 3 表示冒泡阶段 |
preventDefault() |
Function |
只读 | 取消事件的默认行为 。 如 果 cancelable 是 true ,则可以使用这个方法 |
stopImmediatePropagation() |
Function |
只读 | 取消事件的进一步捕获或冒泡,同时阻止任何事件处理程序被调用(DOM3 级事件中新增) |
stopPropagation() |
Function |
只读 | 取消事件的进一步捕获或冒泡。如果bubbles 为true ,则可以使用这个方法 |
target |
Element |
只读 | 事件的目标 |
trusted |
Boolean |
只读 | 为true 表示事件是浏览器生成的。为false 表示事件是由开发人员通过 JavaScript 创建的(DOM3级 事件中新增) |
type |
String |
只读 | 被触发的事件的类型 |
view |
AbstractView |
只读 | 与事件关联的抽象视图。等同于发生事件的 window 对象 |
在需要通过一个函数处理多个事件时,可以使用 type
属性。例如:
var btn = document.getElementById("myBtn");
var handler = function(event){
switch(event.type){
case "click":
alert("Clicked");
break;
case "mouseover":
event.target.style.backgroundColor = "red";
break;
case "mouseout":
event.target.style.backgroundColor = "";
break;
}
};
btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;
这个例子定义了一个名为 handler
的函数,用于处理 3 种事件:click
、mouseover
和 mouseout
。
当单击按钮时,会出现一个与前面例子中一样的警告框。当按钮移动到按钮上面时,背景颜色应该会变成红色,而当鼠标移动出按钮的范围时,背景颜色应该会恢复为默认值。这里通过检测 event.type
属性,让函数能够确定发生了什么事件,并执行相应的操作。
IE 中的事件对象
在使用 DOM0 级
方法添加事件处理程序时,event
对象作为 window
对象的一个属性存在。来看下面的例子。
var btn = document.getElementById("myBtn");
btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};
我们通过 window.event
取得了 event
对象,并检测了被触发事件的类型(IE
中的 type
属性与DOM
中的 type
属性是相同的)。
如果事件处理程序是使用
attachEvent()
添加的,那么就会有一个event
对象作为参数被传入事件处理程序函数中,如下所示。
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
alert(event.type); //"click"
});
因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为 this
会始终等于事件目标。故而,最好还是使用 event.srcElement
比较保险。例如:
属性/方法 | 类 型 | 读/写 | 说 明 |
---|---|---|---|
cancelBubble |
Boolean |
读/写 | 默认值为false ,但将其设置为true 就可以取消事件冒泡(与DOM 中的stopPropagation() 方法的作用相同) |
returnValue |
Boolean |
读/写 | 默认值为true ,但将其设置为false 就可以取消事件的默认行为(与DOM 中的preventDefault() 方法的作用相同) |
srcElement |
Element |
只读 | 事件的目标(与DOM 中的target 属性相同) |
type |
String |
只读 | 被触发的事件的类型 |
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert(window.event.srcElement === this); //true
};
var btn = document.getElementById("myBtn");
btn.onclick = function(){
alert(window.event.srcElement === this); //true
};
btn.attachEvent("onclick", function(event){
alert(event.srcElement === this); //false
// this => window
});
returnValue
属性相当于DOM
中的preventDefault()
方法,它们的作用都是取消给定事件的默认行为。只要将returnValue
设置为false
,就可以阻止默认行为。来看下面的例子。
var link = document.getElementById("myLink");
link.onclick = function(){
window.event.returnValue = false;
};
跨浏览器的事件对象
虽然 DOM
和 IE
中的event
对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。IE
中 event
对象的全部信息和方法 DOM
对象中都有,只不过实现方式不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。可以对前面介绍的 EventUtil
对象加以增强,添加如下方法以求同存异。
var EventUtil = {
addHandler: function(element, type, handler){
//省略的代码
},
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;
}
},
removeHandler: function(element, type, handler){
//省略的代码
},
stopPropagation: function(event){
if (event.stopPropagation){
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
事件类型
UI事件
load
:当页面完全加载后在 window
上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在<img>
元素上面触发,或者当嵌入的内容加载完毕时在<object>
元素上面触发。
unload
:当页面完全卸载后在window
上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在<object>
元素上面触发。
abort
:在用户停止下载过程时,如果嵌入的内容没有加载完,则在<object>
元素上面触发。
error
:当发生JavaScript
错误时在 window
上面触发,当无法加载图像时在<img>
元素上面触发,当无法加载嵌入内容时在<object>
元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。
select
:当用户选择文本框(<input>
或<texterea>
)中的一或多个字符时触发。
resize
:当窗口或框架的大小变化时在 window
或框架上面触发。
scroll
:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。 <body>
元素中包含所加载页面的滚动条。
鼠标与滚轮事件
click
:在用户单击主鼠标按钮(一般是左边的按钮)或者按下回车键时触发。这一点对确保易访问性很重要,意味着onclick
事件处理程序既可以通过键盘也可以通过鼠标执行。
dblclick
:在用户双击主鼠标按钮(一般是左边的按钮)时触发。从技术上说,这个事件并不是DOM2 级事件
规范中规定的,但鉴于它得到了广泛支持,所以 DOM3 级事件
将其纳入了标准。
mousedown
:在用户按下了任意鼠标按钮时触发。不能通过键盘触发这个事件。
mouseenter
:在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。 DOM2 级事件
并没有定义这个事件,但 DOM3 级事件
将它纳入了规范。 IE
、 Firefox 9+
和Opera
支持这个事件。
mouseleave
:在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。 DOM2 级事件
并没有定义这个事件,但DOM3 级事件
将它纳入了规范。IE
、 Firefox 9+
和 Opera
支持这个事件。
mousemove
:当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。
mouseout
:在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
mouseover
:在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。不能通过键盘触发这个事件。
mouseup
:在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。
页面上的所有元素都支持鼠标事件。除了
mouseenter
和mouseleave
,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。
只有在同一个元素上相继触发 mousedown
和 mouseup
事件,才会触发 click 事件;如果 mousedown
或 mouseup
中的一个被取消,就不会触发click
事件。类似地,只有触发两次 click
事件,才会触发一次dblclick
事件。如果有代码阻止了连续两次触发 click
事件(可能是直接取消click
事件,也可能通过取消 mousedown
或 mouseup
间接实现),那么就不会触发 dblclick
事件了。
这 4 个事件触发的顺序始终如下:
(1)mousedown
(2) mouseup
(3)click
(4) mousedown
(5) mouseup
(6)click
(7) dblclick
客户端的坐标位置
鼠标事件都是在浏览器视口中的特定位置上发生的。这个位置信息保存在事件对象的 clientX
和clientY
属性中。所有浏览器都支持这两个属性,它们的值表示事件发生时鼠标指针在视口中的水平
和垂直坐标。
页面的坐标位置
通过客户区坐标能够知道鼠标是在视口中什么位置发生的,而页面坐标通过事件对象的pageX
和 pageY
属性,能告诉你事件是在页面中的什么位置发生的。换句话说,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。
屏幕坐标位置
鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过 screenX
和 screenY
属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。
HTML5 事件
很多浏览器出于不同的目的——满足用户需求或解决特殊问题,还实现了一些自定义的事件。HTML5
详尽列出了浏览器应该支持的所有事件。
contextmenu 事件
为了实现上下文菜单,开发人员面临的主要问题是如何确定应该显示上下文菜单(在Windows
中,是右键单击;在 Mac
中,是 Ctrl+
单击),以及如何屏蔽与该操作关联的默认上下文菜单。为解决这个问题,就出现了contextmenu
这个事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义的菜单。
由于contextmenu
事件是冒泡的,因此可以为document
指定一个事件处理程序,用以处理页面中发生的所有此类事件。这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在兼容DOM
的浏览器中,使用 event.preventDefalut()
;在IE
中,将 event.returnValue
的值设置为false
。
EventUtil.addHandler(window, "load", function(event){
var div = document.getElementById("myDiv");
EventUtil.addHandler(div, "contextmenu", function(event){
event = EventUtil.getEvent(event);
EventUtil.preventDefault(event);
var menu = document.getElementById("myMenu");
menu.style.left = event.clientX + "px";
menu.style.top = event.clientY + "px";
menu.style.visibility = "visible";
});
EventUtil.addHandler(document, "click", function(event){
document.getElementById("myMenu").style.visibility = "hidden";
});
});
DOMContentLoaded 事件
如前所述, window
的load
事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而颇费周折。而DOMContentLoaded
事件则在形成完整的DOM
树之后就会触发,不理会图像、 JavaScript
文件、CSS
文件或其他资源是否已经下载完毕。
要处理 DOMContentLoaded
事件,可以为 document
或 window
添加相应的事件处理程序(尽管这个事件会冒泡到 window
,但它的目标实际上是document
)。来看下面的例子。
EventUtil.addHandler(document, "DOMContentLoaded", function(event){
alert("Content loaded");
});
readystatechange 事件
IE
为 DOM 文档中的某些部分提供了 readystatechange 事件。这个事件的目的是提供与文档或元素的加载状态有关的信息,但这个事件的行为有时候也很难预料。支持 readystatechange 事件的每个对象都有一个 readyState 属性,可能包含下列 5 个值中的一个。
uninitialized(未初始化):对象存在但尚未初始化。
loading(正在加载):对象正在加载数据。
loaded(加载完毕):对象加载数据完成。
interactive(交互):可以操作对象了,但还没有完全加载。
complete(完成):对象已经加载完毕。
对于 document
而言,值为"interactive"
的readyState
会在与 DOMContentLoaded
大致相同的时刻触发readystatechange
事件。此时,DOM
树已经加载完毕,可以安全地操作它了,因此就会进入交互(interactive
)阶段。但与此同时,图像及其他外部文件不一定可用。
ventUtil.addHandler(document, "readystatechange", function(event){
if (document.readyState == "interactive"){
alert("Content loaded");
}
});
交互阶段可能会早于也可能会晚于完成阶段出现,无法确保顺序。在包含较多外部资源的页面中,交互阶段更有可能早于完成阶段出现;而在页面中包含较少外部资源的情况下,完成阶段先于交互阶段出现的可能性更大。因此,为了尽可能抢到先机,有必要同时检测交互和完成阶段,如下面的例子所示。
EventUtil.addHandler(document, "readystatechange", function(event){
if (document.readyState == "interactive" || document.readyState == "complete"){
EventUtil.removeHandler(document, "readystatechange", arguments.callee);
alert("Content loaded");
}
});
支持
readystatechange
事件的浏览器有IE
、Firfox 4+
和Opera
。
虽然使用
readystatechange
可以十分近似地模拟DOMContentLoaded
事件,但它们本质上还是不同的。在不同页面中,load
事件与readystatechange
事件并不能保证以相同的顺序触发。
内存和性能
由于事件处理程序可以为现代 Web
应用程序提供交互能力,因此许多开发人员会不分青红皂白地向页面中添加大量的处理程序。在创建GUI
的语言(如C#
)中,为GUI
中的每个按钮添加一个 onclick
事件处理程序是司空见惯的事,而且这样做也不会导致什么问题。可是在 JavaScript
中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面的。
首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的 DOM
访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的
角度出发,还是有一些方法能够提升性能的。
事件委托
如果在一个复杂的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;
}
});
移除事件处理程序
每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript
代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler
),也是造成Web
应用程序内存与性能问题的主要原因。
如果你知道某个元素即将被移除,那么最好手工移除事件处理程序,如下面的例子所示。
btn.onclick = function(){
//先执行某些操作
btn.onclick = null; //移除事件处理程序
document.getElementById("myDiv").innerHTML = "Processing...";
}
在此,我们在设置<div>
的innerHTML
属性之前,先移除了按钮的事件处理程序。这样就确保了内存可以被再次利用,而从 DOM
中移除按钮也做到了干净利索。注意,在事件处理程序中删除按钮也能阻止事件冒泡。目标元素在文档中是事件冒泡的前提。
参考文献
《javascript 高级程序设计3》