49. 用 JavaScript 处理事件
引言
现在你应该能够得心应手地用 CSS 来进行样式化和页面布局了,而且也蹒跚地迈出了使用变量,函数,方法等的第一步。现在我们该开始运用这些知识来为我们的网站用户提供交互性和动态特性(比如说拖放与拖拽,动画,等等)了。用 JavaScript 来对事件进行控制,你就可以像 Frankenstein 博士一样,为你的作品添加活力了。
我们对 JavaScript 的乐趣已经谈的够多了——本文将会更注重实际一些,告诉你什么是事件,以及如何在网页中利用事件。本文目录如下:
别忘了,你可以点击此处下载本文的代码示例,并自行尝试。
什么是事件?
当 Web 页面中发生了某些类型的交互时,事件就发生了。事件可能是用户在某些内容上的点击、鼠标经过某个特定元素或按下键盘上的某些按键。事件还可能是 Web 浏览器中发生的事情,比如说某个 Web 页面加载完成,或者是用户滚动窗口或改变窗口大小。
通过使用 JavaScript ,你可以监测特定事件的发生,并规定让某些事情发生以对这些事件做出响应。
事件是如何工作的
如果事件发生在 Web 页面的某个HTML元素上,它就会检查该元素是否绑定有任何事件处理器。如果有的话,就会按顺序分别调用这些事件处理器,并同时发送发生的事件的引用和补充信息。然后事件处理器就会对该事件进行处理。
事件顺序有两种类型:事件捕捉和事件冒泡。
事件捕捉是由 DOM中 最外部的元素开始的,向内直到该事件发生之所在的HTML元素。比如说,在某个 Web 页面上的点击事件可能会首先检查 HTML
元素的 onclick
事件处理器,然后检查 body
元素,依次类推,直到它到达该事件的目标为止。
事件冒泡的工作方式则是刚好相反:它首先会检查该事件的目标有没有绑定任何的事件处理器,然后往上冒泡,分别经过每个父元素,直到它到达 HTML 元素为止。
事件的演变
在 JavaScript 编程的早期,我们直接将事件处理器放在 HTML 元素之内来使用,就像这样:
<a href="http://www.opera.com/" onclick="alert('Hello')">Say hello</a>
这种方法的问题在于它导致了事件处理器散布在整个代码中,无法集中控制,并且无法像外部 JavaScript 文件那样利用浏览器的缓存功能。
在事件的演变史中接下来的用法是将事件放在一个 JavaScript 代码块中,比如说:
<script type="text/JavaScript">
document.getElementById("my-link").onclick = waveToAudience;
function waveToAudience() {
alert("Waving like I've never waved before!");
}
</script>
<a id="my-link" href="http://www.opera.com/">My link</a>
注意上面的例子中清爽的 HTML 代码。一般的,这种用法被称作非干扰性的 JavaScript 。这样的优点除了能利用 JavaScript 的缓存和便于控制代码之外,好处还有代码分离:所有的页面内容放在一个地方,而页面交互的代码放在另一个地方。这样做还可以提供一种可访问性更好的方式,使得链接即使在 JavaScript 被禁用的时候也能很好地工作;这样的做法也更受搜索引擎欢迎。
DOM Level 2 Events 规范
在2000年11月的时候,W3C 推出了文档对象模型(DOM)Level 2 Events 规范,该规范提供了一种更详细的更细致的方式以控制 Web 页面中的事件。这种新的将事件应用到HTML元素上的方法如下:
document.getElementById("my-link").addEventListener("click", myFunction, false);
addEventListener
方法的第一个参数就是该事件的名字,应该注意的是这里不再使用“on”前缀了。第二个参数是想要在该事件发生时调用的函数的引用。第三个参数控制了该事件的所谓的 useCapture
,比如说,该使用事件捕捉还是事件冒泡。
与 addEventListener
对应的是 removeEventListener
,后者会从 HTML 元素中删除掉所应用的任何事件。
Internet Explorer 事件模型
遗憾的是,IE 到目前为止还没有实现 DOM Level 2 事件模型,IE 用的是它自己私有 attachEvent
方法。该方法用法如下:
document.getElementById("my-link").attachEvent("onclick", myFunction);
注意 attachEvent
在实际事件名的前面仍然使用了“on”前缀,而且它也没有包含任何支持以确定使用捕获阶段。
与 attachEvent
相对应的是 detachEvent
,后者会从 HTML 元素中删除掉所应用的任何事件。
事件的跨浏览器应用
由于浏览器之间在事件处理实现上的不一致, Web 开发者对于提供一种跨越所有主流浏览器的解决方案进行了大量尝试。这些解决方案各有其优缺点,通常我们把它们叫做 addEvent
函数。
大多数主流的 JavaScript 程序库都有内嵌这些函数,此外在网上还可以找到许多单独的解决方案。我的一个建议是使用 Dean Edwards 的 addEvent
;你也可以考虑一下类似于 jQuery JavaScript 库的事件处理这样的解决方案。
事件与可访问性
在我们更深入地说明如何控制和调用事件之前,我想强调一下可访问性。可访问性对大多数人来讲是一个广义的概念,在这里我是用它来表达一个意思,就是你想通过事件的使用来实现的功能应该在 JavaScript 被禁用的时候,或者是由于其它原因导致 Web 浏览器无法使用 JavaScript 的时候,仍然能够工作。
有些人确实是有意关掉自己的 Web 浏览器中的 JavaScript 的,但更常见是的代理服务器,防火墙,以及过度敏感的防病毒程序阻止了 JavaScript 的正常运行。千万别受这些特殊情况打击;我的目的就是教你创建包含可访问的备用版本的事件,以防 JavaScript 不可用的情况。
一般而言,不要在那些不具备默认行为的 HTML 元素上应用事件。你只能对a
这样的元素应用 onclick
事件,因为该元素已经具备了针对点击事件的备用行为(比如,转到该链接所指定的位置,或提交表单)。
事件控制
我们先来看一个关于事件的简单例子,演示如何对其做出反应。为了保持简单,我会用前面提过的 addEvent
方案,免得在每个例子中都要牵扯到错综复杂跨浏览器措施。
我们的第一个例子是 onload
事件,该事件属于 window
对象。一般而言,任何会影响浏览器窗口的事件(比如说 onload
,onresize
和 onscroll
)都可以通过 window
对象来获取。
Web 页面上的所有内容加载完毕时就会触发 onload
事件,这里的内容包括 HTML 代码以及像图像, CSS 文件和 JavaScript 文件之类的外部依赖。所有这一切都加载完毕后,就会调用 window.onload
,这样你就触发相应的 Web 页面功能了。下面这个非常简单的示例在页面加载完毕的时候弹出一个警报消息框:
addEvent(window, "load", sayHi);
function sayHi() {
alert("Hello there, stranger!");
}
还不错,对吧?如果你愿意的话,你还可以用所谓的匿名函数来代替这种方式,就不需要函数名了。如此一来,上面的代码就变成了这样:
addEvent(window, "load", function () {
alert("Hello there, stranger!");
});
将事件应用到特定元素上
为了更深入地阐述这个问题,我们先来看看如何将事件添加到页面中其它元素上去。我们假设你想让一个事件在每次点击链接的时候都发生。联系前面所学到的知识,也许我们可以这样来处理:
addEvent(window, "load", function () {
var links = document.getElementsByTagName("a");
for (var i=0; i<links.length; i++) {
addEvent(links[i], "click", function () {
alert("NOPE! I won't take you there!");
// This line's support added through the addEvent function. See below.
evt.preventDefault();
});
}
});
OK,刚刚发生了什么?首先,我们使用了 onload
事件以检测 Web 页面是否加载完毕。然后通过使用 document
对象的 getElementsByTagName
方法,我们找出了该页面中的所有链接。我们在所有链接中循环,并将事件应用到这些链接上,这样,一旦这些链接被点击就会引发某个操作。
“won’t take you there”部分之后的代码又是怎么回事呢?在显示 alert
信息之后,它下面的一行代码的作用如同 return false
。它的意思是,返回 false
来阻止默认操作的运行。在本文最后的部分中我们还会涉及此问题。
事件对象引用
为了更好的处理事件,你可以根据所发生的事件的特定属性来采取不同的操作。比如说,如果你要处理 onkeypress
事件,你可能会希望该事件只在用户按下 enter 键的时候才发生,而在按下其他键时不发生。
如事件模型一样,IE 和其他浏览器处理方法不同:IE 使用一个叫做 event
的全局事件对象来处理对象,而其它所有浏览器采用的 W3C 推荐的方式,仅传递特定事件的事件对象爱你个。跨浏览器实现这样的功能时,最常见的问题就是获取事件本身的引用及获取该事件的目标元素的引用。下面这段代码就为你解决了这个问题:
addEvent(document.getElementById("check-it-out"), "click", eventCheck);
function eventCheck (evt) {
var eventReference = (typeof evt !== "undefined")? evt : event;
var eventTarget = (typeof eventReference.target !== "undefined")? eventReference.target : eventReference.srcElement;
}
eventCheck
函数中的第一行代码会检查是否有传递某个事件对象给该函数。如果有的话,该事件对象就会自动成为该函数的第一个参数,在本例中就是 evt
。如果不存在这样的事件对象的话,意味当前的 Web 浏览器是IE,该函数就会引用 window
对象的 event
全局属性。
第二行代码会在已经建立好的事件引用的基础上查找 target
属性。如果该属性不存在,则会使用 IE 实现的 srcElement
属性。
注:上文提到的 addEvent
函数中也解决了此问题,该函数中事件对象在所有的 Web 浏览器中有完全相同的工作方式。不过,上面的代码故意忽视 addEvent 函数这一特点,为了让你对 Web 浏览器之间的差异有个深刻理解。
查看事件的某个属性
让我们来付诸实践。下面的例子会根据所按的键执行不同的代码:
addEvent(document.getElementById("user-name"), "keyup", whatKey);
function whatKey (evt) {
var eventReference = (typeof evt !== "undefined")? evt : event;
var keyCode = eventReference.keyCode;
if (keyCode === 13) {
// The Enter key was pressed
// Code to validate the form and then submit it
}
else if (keyCode === 9) {
// The Tab key was pressed
// Code to, perhaps, clear the field
}
}
whatKey
函数内部的代码会检查所发生的事件的特定属性,也就是 keyCode
,来看看到底按下的是键盘上的哪个按键。数字13的意思是 Enter 键,而数字9代表 Tab 键。
事件默认行为和事件冒泡
在许多情况下你可能需要阻止某个事件的默认行为。比如说,你可能想在特定区域未被填写的情况下阻止用户提交表单。同样的,有时你可能会需要阻止事件冒泡,本文的这一部分讲述的就是在这种情况下如何阻止事件默认行为和事件冒泡:
阻止事件的默认行为
就像事件模型和事件对象差异一样,在IE和其它所有浏览器中阻止事件的默认行为的方法也不同。用前面的代码作为获取事件对象引用的基础,下面的代码介绍如何在链接被点击时,阻止链接默认行为:
addEvent(document.getElementById("stop-default"), "click", stopDefaultBehavior);
function stopDefaultBehavior (evt) {
var eventReference = (typeof evt !== "undefined")? evt : event;
if (eventReference.preventDefault) {
eventReference.preventDefault();
}
else {
eventReference.returnValue = false;
}
}
这种方法用到了叫做对象检测的技术,以在某个方法被调用之前先确认它是否真正可用,这样做有助于防止潜在的错误。这里的 preventDefault
方法在除 IE 外的所有浏览器中都可用,它可以阻止事件的默认行为的发生。
若此方法不被支持,该函数将全局事件对象的 returnValue
设置为 false
,从而阻止该事件的默认行为在 IE 中的运行。
阻止事件冒泡
考虑下面的 HTML 代码:
<div>
<ul>
<li>
<a href="http://www.opera.com/">Opera</a>
</li>
<li>
<a href="http://www.opera.com/products/dragonfly/">Opera Dragonfly</a>
</li>
</ul>
</div>
假设你在所有的 a
元素,li
元素和 ul
元素上都应用了 onclick
事件。onclick
事件首先调用的是链接的事件处理器,然后是列表项,最后调用的是无序列表的事件处理器。
用户点击链接时,你很有可能不需要调用其父元素 li
元素的事件处理器,只需要使用户跳转至对应页面去。不过,如果用户点击的是该链接旁边的 li
项,你可能会想要触发 li
的事件处理器,同时也触发 ul
的事件处理器。
注意,在DOM level 2 模型和 useCapture
都启用的情况下,也就是使用事件捕捉时,事件处理就会从无序列表开始,然后是列表项,最后才是链接。然而由于 IE 不支持事件捕捉,实际应用中很少使用该功能。
关于如何编写代码以阻止某个事件的冒泡,可以参照下面的程序:
addEvent(document.getElementById("stop-default"), "click", cancelEventBubbling);
function cancelEventBubbling (evt) {
var eventReference = (typeof evt !== "undefined")? evt : event;
if (eventReference.stopPropagation) {
eventReference.stopPropagation();
}
else {
eventReference.cancelBubble = true;
}
}
完整的事件处理示例
我在示例页面中,演示了如何添加事件处理器,及在某种条件下阻止事件的默认行为。。这例子中的事件处理器会根据用户是否填写完所有的表单域来确定是否可以提交表单。本示例的 JavaScript 代码如下:
addEvent(window, "load", function () {
var contactForm = document.getElementById("contact-form");
if (contactForm) {
addEvent(contactForm, "submit", function (evt) {
var firstName = document.getElementById("first-name");
var lastName = document.getElementById("last-name");
if (firstName && lastName) {
if (firstName.value.length === 0 || lastName.value.length === 0) {
alert("You have to fill in all fields, please.");
evt.preventDefault();
}
}
});
}
});
总结
本文仅仅只触及了事件处理的毛皮,但我希望你能理解事件是如何工作的。对初学者来说,本文中关于浏览器不一致性的部分可能有点太难了,但我相信从一开始就知晓兼容性问题是非常重要的。
一旦你领会了这些问题,并掌握了前面所说的那些解决方案,你通过 JavaScript 和事件处理所能实现的效果将是无限量的!
练习题
- 什么是事件?
- 事件捕捉和事件冒泡之间的差别是什么?
- 有没有可能对一个事件的执行加以控制,比如阻止其默认行为?该怎么做?
- attachEvent 的主要问题和使用范围是什么?