12.2 事件处理程序 (或事件侦听器)
事件就是用户或浏览器自身执行的某种动作。诸如 click、load 和 mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序 (或事件侦听器)。事件处理程序的名字以 "on" 开头,因此 click 事件的事件处理程序就是 onclick,load 事件的事件处理程序就是 onload。为事件指定处理程序的方式有好几种。
12.2.1 HTML 事件处理程序
某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。这个特性的值应该是能够执行的 JavaScript 代码。例如,要在按钮被单击时执行一些JavaScript,可以像下面这样编写代码:
<input type="button" value="Click Me" onclick="alert('Clicked')" />
当单击这个按钮时,就会显示一个警告框。这个操作是通过指定 onclick 特性并将一些 JavaScript 代码作为它的值来定义的。由于这个值是 JavaScript,因此不能在其中使用未经转义的 HTML 语法字符,例如和号 (&)、双引号 ("")、小于号(<)或大于号(>)。为了避免使用 HTML 实体,这里使用了单引号。如果想要使用双引号,那么就要将代码改写成如下所示:
<input type="button" value="Click Me" onclick="alert("Clicked")" />
在HTML中定义的事件处理程序可以包含要执行的具体动作,也可以调用在页面其他地方定义的脚本,如下面的例子所示:
<script type="text/javascript">
function showMessage(){
alert("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" />
在这个例子中,单击按钮就会调用 showMessage() 函数。这个函数是在一个独立的 <script> 元素中定义的,当然也可以被包含在一个外部文件中。事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
不过,在HTML中指定事件处理程序有两个缺点。首先,存在一个时差问题。因为用户可能会在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。以前面的例子来说,假设 showMessage() 函数是在按钮下方、页面的最底部定义的。如果用户在页面解析 showMessage() 函数之前就单击了按钮,就会引发错误。为此,很多HTML事件处理程序都会被封装在一个 try-catch 块中,以便错误不会浮出水面,如下面的例子所示:
<input type="button" value="Click Me" onclick=“ try{showMessage();}catch(ex){}” />
这样,如果在 showMessage() 函数有定义之前单击了按钮,用户将不会看到 JavaScript 错误,因为在浏览器有机会处理错误之前,错误就被捕获了。
通过HTML指定事件处理程序的第二个缺点是 HTML 与 JavaScript 代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML 代码和 JavaScript 代码。而这正是许多开发人员摒弃 HTML 事件处理程序,转而使用 JavaScript 指定事件处理程序的原因所在。
12.2.2 DOM0级事件处理程序
通过 JavaScript 指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。要使用 JavaScript 指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素 (包括 window 和 document) 都有自己的事件处理程序属性,这些属性通常全部小写,例如 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取得的。不仅仅是 ID,实际上可以在事件处理程序中通过 this 访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
也可以删除通过 DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为 null 即可:
btn.onclick = null; // 删除事件处理程序
将事件处理程序设置为 null 之后,再单击按钮将不会有任何动作发生。
如果你使用 HTML 指定事件处理程序,那么 onclick 属性的值就是一个包含着在同名 HTML 特性中指定的代码的函数。而将相应的属性设置为 null,也可以删除以这种方式指定的事件处理程序。
12.2.3 DOM2级事件处理程序
“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener()。所有 DOM 节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。
要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
上面的代码为一个按钮添加了 onclick 事件处理程序,而且该事件会在冒泡阶段被触发 (因为最后一个参数是 false)。与 DOM0 级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。来看下面的例子:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
btn.addEventListener("click", function(){
alert("Hello world!");
}, false);
这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发,因此首先会显示元素的ID,其次会显示 "Hello world!" 消息。
通过 addEventListener() 添加的事件处理程序只能使用 removeEventListener() 来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过 addEventListener() 添加的匿名函数将无法移除,如下面的例子所示:
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert(this.id);
}, false);
// 这里省略了其他代码
btn.removeEventListener("click", function(){ // 没有用!
}, false);
在这个例子中,我们使用 addEventListener() 添加了一个事件处理程序。虽然调用 removeEventListener() 时看似使用了相同的参数,但实际上,第二个参数与传入 addEventListener() 中的那一个是完全不同的函数。而传入 removeEventListener() 中的事件处理程序函数必须与传入 addEventListener() 中的相同,如下面的例子所示:
var btn = document.getElementById("myBtn");
var handler = function(){
alert(this.id);
};
btn.addEventListener("click", handler, false);
// 这里省略了其他代码
btn.removeEventListener("click", handler, false); // 有效!
重写后的这个例子没有问题,是因为在 addEventListener() 和 removeEventListener() 中使用了相同的函数。
大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,我们不建议在事件捕获阶段注册事件处理程序。
Firefox、Safari、Chrome 和 Opera 支持 DOM2 级事件处理程序。
12.2.4 IE 事件处理程序
IE 实现了与DOM中类似的两个方法:attachEvent() 和 detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于IE只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序都会被添加到冒泡阶段。
要使用 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
});
在编写跨浏览器的代码时候,牢记这一区别非常重要。
与 addEventListener() 类似,attachEvent() 方法也可以用来为一个元素添加多个事件处理程序。来看下面的例子:
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert("Clicked");
});
btn.attachEvent("onclick", function(){
alert("Hello world");
});
这里调用了两次 attachEvent(),为同一个按钮添加了两个不同的事件处理程序。不过,与DOM 方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。单击这个例子中的按钮,首先看到的是 "Hello world!",然后才是 “Clicked” 。
使用 attachEvent() 添加的事件可以通过 detachEvent()来移除,条件必须提供相同的参数。与DOM 方法一样,这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给detachEvent(),就可以移除相应的事件处理程序。例如:
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
btn.attachEvent("onclick", handler);
// 这里省略了其他代码
btn.detachEvent("onclick", handler);
这个例子将保存在变量 handler 中的函数作为事件处理程序。因此,后面的 detachEvent() 可以使用相同的函数来移除事件处理程序。
12.2.5 跨浏览器的事件处理程序
为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的 JavaScript 库,还有一些开发人员会自己开发最合适的事件处理的方法。自己编写代码其实也不难,只要恰当地使用能力检测即可。要保证处理事件的代码能在大多数浏览器下一致地运行,只须关注冒泡阶段。
第一个要创建的方法是 addHandler(),它的职责是视情况分别使用DOM0级方法、DOM2级方法或IE方法来添加事件。这个方法属于一个名叫 EventUtil 的对象,本书将使用这个对象来处理浏览器间的差异。addHandler()方法接受3个参数:要操作的元素、事件名称和事件处理程序函数。
与 addHandler() 对应的方法是 removeHandler(),它也接受相同的参数。这个方法的职责是移除之前添加的事件处理程序 -- 无论该事件处理程序是采取什么方式添加到元素中的,如果其他方法无效,默认采用 DOM0级方法。
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;
}
}
};
这两个方法首先都会检测传入的元素是否存在 DOM2级方法。如果存在 DOM2级方法,则使用该方法:传入事件类型、事件处理程序函数和第三个参数 false (表示冒泡阶段)。如果存在IE的方法,则采取第二种方案。注意此时的事件类型必须加上 "on" 前缀。最后一种可能就是使用 DOM0 级方法 (在现代浏览器中,应该不会执行这里的代码)。此时,我们使用的是方括号语法来将属性名指定为事件处理程序,或者将属性设置为 null。
可以像下面这样使用 EventUtil 对象:
var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
EventUtil.addHandler(btn, "click", handler);
// 这里省略了其他代码
EventUtil.removeHandler(btn, "click", handler);
addHandler() 和 removeHandler() 没有考虑到所有的浏览器问题,例如在IE中的作用域问题,不过,使用它们添加和移除事件处理程序还是足够了。