事件

本文是对 JavaScript 高级程序设计 一书中相关章节的整理,加上自己的理解


1. 定义

事件就是用户或浏览器自身执行的某种动作,比如,页面加载完毕,鼠标点击,获得焦点等等。

JavaScript 与 HTML 之间的交互是通过事件实现的。

可以使用侦听器(事件处理程序,有时也叫监听函数或句柄)来预订事件,当特定事件发生时,执行相应的侦听器中的代码。

在 W3C 对 DOM 事件进行规范化之前的事件处理,一般称为 DOM0 级事件处理
W3C 在 DOM2 级文档规范中,包含了 DOM 事件(DOM Events)规范,也就是我们通常所说的 DOM2 级事件处理

2. 事件流

事件流描述的是当事件被触发时,相应节点接收事件的顺序。事件流有两种类型:事件冒泡和事件捕获。

2.1 IE 的事件流 — 事件冒泡

事件冒泡,即事件被触发时,由最具体的元素(文档中嵌套层次最深的那个节点)先就收事件,然后逐级向上传播到较为不具体的节点(最不具体的节点是文档节点)。以下面的 HTML 页面为例:

<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>

如果你单击了页面中的 div 元素,那么这个 click 事件会按照:
<div><body><html> → document
的顺序传播,并且事件会依次在这四个节点上发生,直至传播到 document 对象上。如果节点上有相应监听函数,则会被执行。

2.2 网景的事件流 — 事件捕获

事件捕获跟事件冒泡正好相反,即不太具体的节点应该最早接受到事件,而最具体的节点最晚接受到事件。
2.1 中的例子,在这里事件会按照:
document → <html><body><div>
的顺序传播并发生

2.3 W3C 规范事件流 — DOM 事件流

这两个浏览器采用截然相反的事件流给开发者带来了极大的痛苦,后来 W3C 在 DOM2 级文档中对事件流进行了规范,推出 DOM 事件流
DOM 事件流包含三个阶段:

  1. 事件捕获阶段
    为截获事件提供了机会
  2. 处于目标阶段
    实际的目标接收到事件
  3. 事件冒泡阶段
    在这个阶段对事件做出响应

现代浏览器 IE9、 Opera、 Firefox、 Chrome 和 Safari 都支持 DOM 事件流,只有 IE8 及以下版本的 IE 浏览器不支持,采用的还是 IE 自己的之前的冒泡事件流。

规范

按照规范,上文例子中的事件是如下传播的
规范事件流

  • 在 DOM 事件流中,实际的目标(<div>)在捕获阶段是不会接收到事件的。在捕获阶段,事件只传播到 <body>
  • 下一个阶段是处于目标阶段,事件传播到 <div>。但是,在事件处理中,<div> 被作为冒泡阶段的一部分。
  • 最后是冒泡阶段。

现实

现代浏览器对 DOM 事件流的支持与规范有两点不同:

  1. 即使规范明确要求捕获阶段不会涉及事件目标,但是,IE9、 Safari、 Chrome、 Firefox 和 Opera 9.5 及更高版本都会在捕获阶段触发事件对象上的事件。
    也就是说在事件处理中,目标节点即被当做冒泡阶段的一部分(规范规定),又被当做捕获阶段的一部分(浏览器支持)。
    所以,如果目标对象上有一个侦听器,在事件捕获和事件冒泡都没有关闭的情况下,这个侦听器中的函数会被执行两次。或者可以设置两个侦听器,一个对应事件捕获,一个对应事件冒泡,这样,事件发生时两个侦听器中的函数都会被触发。

  2. window 对象开始捕获事件。

按照 浏览器的支持,上文例子中的事件是如下传播的:

实际事件流

3. 事件处理程序

3.1 HTML 事件处理程序

某个元素支持的每种事件,都可以在 HTML 文档中用一个与相应事件处理程序同名的特性(attribute)来指定。特性的值是能够执行的 JavaScript 代码。例如:

<input type="button" value="Click me" onclick="alert('clicked')">

当单击这个按钮时,就会显示一个警告框。

虽然特性的值是 JavaScript 代码,但是它仍然是在 HTML 文档中,所以不能包含 HTML 中的特殊字符,例如,和好(&)、双引号(””)、小于号(<)、大于号(>)。如果包含 HTML 特殊字符,则需要转义成 HTML 实体字符。其中,双引号可以改用单引号来避免转义。

如果想用双引号,则上面的代码要改写成:

<input type="button" value="Click me" onclick="alert(&quot;clicked&quot;)">

事件处理程序中的代码在执行时,有权访问全局作用域中的所有代码。因此,特性的值既可以是具体执行的代码,也可以通过调用页面其他地方定义的脚本来执行脚本如下面的例子:

<script type="text/javascript">
function showMessage(){
alert("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" />

缺点

  1. 时差问题,当事件处理程序调用的是页面其他部分或外部脚本时,触发事件时,脚本可能还没有加载玩不
  2. HTML 与 JavaScript 紧密耦合,不利于维护。

3.2 DOM0 级事件处理程序

这是通过 JavaScript 指定事件处理程序的传统方式,将一个函数赋值给节点的 事件处理程序属性

添加

每个元素节点对象都有自己的事件处理程序属性,将这种属性的值设置为一个函数,就可以指定事件处理程序:

var btn = document.getElementById("myBtn");
btn.onclick = function() {
alert("Clicked");
};

删除

将事件处理程序属性的值设置为 null 即可:

btn.onclick = null;

this 指向

使用 DOM0 级方法指定的事件处理程序被认为是元素的方法。因此,这个时候的事件处理程序是在元素的作用域中运行的;换句话说,程序中的 this 执行当前元素。

兼容性

所有现代浏览器都支持这种方式

优点

  1. 兼容性很好,所有现代浏览器都支持
  2. 将 HTML 和 JavaScript 代码分开

缺点

  1. 只有在 JavaScript 中相应代码执行后,才会给元素添加事件处理程序,存在一定时差
  2. 只能添加一个同名的事件处理程序

3.3 DOM2 级事件处理程序

HTML 和 DOM0 事件处理程序都是没有规范化之前的,但是所有浏览器都还支持。
DOM2 级事件处理程序是 W3C 规范化后的事件处理程序。

“ DOM2 级事件” 定义了两个方法,用于添加和删除事件处理程序:addEventListener() 和 removeEventListener()。

所用 DOM 节点中都包含着两个方法,并且它们都接受 3 个参数,要处理的事件名、作为事件处理程序的函数和一个布尔值。
最后的布尔值,如果是 true,表示在捕获阶段调用事件处理程序;如果是 false,表示在冒泡阶段调用事件处理程序。

添加

addEventListener()
要在按钮上为 click 事件添加事件处理程序,可以使用下列代码:

var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert("Clicked");
}, false);

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。
最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。

删除

removeEventListener()
通过 addEventListener() 添加的事件处理程序只能用 removeEventListener() 来移除,而且传入的参数也要一样。

这里需要注意的是,如果事件处理程序是函数(匿名或具名),即使函数体代码完全一样,这两个函数也是两个不同的函数。被认为是不同的参数,不能被移除。

这是因为函数是一级对象,虽然两个方法中的函数是来自同一个构造函数,但是构造函数的两次调用得到的是不同的两个对象。

function () {};
new Function(); // 与上行代码等价
function a() {};
var a = new Function(); // 与上行代码等价
var btn = document.getElementById("myBtn");
btn.addEventListener("click", function(){
alert("Clicked");
}, false);
btn.removeEventListener("click", function(){ // 无效
alert("Clicked");
}, false);

解决方法是将函数的引用传入两个方法中

function handler(){
alert("Clicked");
}
var btn = document.getElementById("myBtn");
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false); // 有效

this 指向

与 DOM0 级一样,这里添加的事件处理程序也是在其绑定的元素节点的作用域中运行的,所以事件处理程序中的 this 指向绑定的元素节点对象。

兼容性

IE9、 Firefox、 Safari、 Chrome 和 Opera 支持 DOM2 级事件处理程序。
IE8 及以下版本的 IE 浏览器不支持。

优点

  1. 可以为同一个元素添加多个同名事件处理程序,按添加顺序执行
  2. HTML 和 JavaScript 分离

缺点

  1. 跟 DOM0 有一样的时差问题
  2. 兼容性问题,IE8 及以下版本的 IE 浏览器不支持

3.4 IE 事件处理程序

IE8 及以下版本的 IE 浏览器不支持 W3C 规范的 DOM2级事件处理程序。采用的是它自己老的一套事件处理程序

IE 实现了与 W3C 规范中类似的两个方法:attachEvent() 和 detachEvent()。
这两个方法接受两个参数:事件处理程序的名称(带前缀 on,例如 onclick)和事件处理程序函数。通过 attachEvent() 方法添加的事件处理程序都自动添加到冒泡阶段。

添加

attachEvent
要使用 attachEvent() 为按钮添加一个事件处理程序,可以使用下面的代码:

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function() {
alert("Clicked");
});

移除

detachEvent()
跟 removeEventListener() 一样的要求,参数一样,函数只能是引用。

var btn = document.getElementById("myBtn");
var handler = function(){
alert("Clicked");
};
btn.attachEvent("onclick", handler);
btn.detachEvent("onclick", handler);

this 指向

与 DOM0 级和 DOM2 级不同的是,在使用 attachEvent() 方法的情况下,事件处理程序会在全局作用域中运行,因此,this 指向了 window。

var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
alert(this === window); //true
});

优点

  1. 与 DOM2 级一样可以为同一个元素添加几个同名事件处理程序。但是不同的是,执行顺序跟 DOM2 级是相反的,后添加的先执行。
  2. HTML 和 JavaScript 分离

缺点

  1. 最大的缺点就是兼容性问题,只有 IE 和 Opera 支持这种方式
  2. this 指向、同名事件处理程序的执行顺序与其他浏览器不一致。

3.5 跨浏览器的事件处理程序

我们来简单地总结一下:

  • HTML 事件处理程序简单没有时差,所有浏览器都支持,但是结构和行为紧密耦合,不推荐使用。
  • DOM0 级事件处理程序结构和行为分离,可能有时差,所有浏览器都支持,但是同一个节点中只能为一个事件添加一个事件处理程序。
  • DOM2 级事件处理程序在 DOM0 级基础上,可以为同一个节点的同一个事件添加多个事件处理程序,但是 IE8 及以下版本的 IE 浏览器不支持。
  • IE 事件处理程序,有 DOM2 级的功能,但是仅支持冒泡事件流,并且只支持 IE 和 Opera。

我们的解决方案是在 仅支持事件冒泡 的基础上,尽可能让浏览器用后两种方法,不行的话就使用 DOM0 级。下面是代码:

var EventUtil = {
addHandler: function(elem, type, handler) {
if (elem.addEventListener) {
elem.addEventListener(type, handler, false); // 注意设置冒泡事件流
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, handler); // 注意要加前缀
} else {
elem["on" + type] = handler;
}
}
},
removeHandler: function(elem, type, handler) {
if (elem.removeEventListener) {
elem.removeEventListener(type, handler, false);
} else if (elem.detachEvent) {
elem.detachEvent("on" + type, handler);
} else {
elem["on" + type] = null;
}
}
};
  • 上面的代码,首先会检测传入的元素是否存在 DOM2 级方法。(IE9、 Firefox、 Safari、 Chrome 和 Opera 会采用这种方法)
  • 如果不支持 DOM2 级,说明是 IE8 或以下版本的 IE 浏览器,则采用 IE 事件处理程序。
  • 如果还不支持的话,最后采用 DOM0 级。

EventUtil 对象的使用如下:

var btn = document.getElementById("myBtn");
var handler = function() {
alert("Clicked");
};
EventUtil.addHandler(btn, click, handler);
EventUtil.removeHandler(btn, click, handler);

需要注意的是,addHandler()和 removeHandler() 没有考虑到所有的浏览器问题,比如说 IE 事件处理程序中的作用域问题。
不过,使用它们添加和移除事件处理程序还是足够了。

4. 事件对象

当 DOM 上的某个事件被触发时,会产生一个 event 事件对象,这个对象中包含着所有与事件相关的信息。比如说导致事件的元素(目标元素)、事件的类型等。
所有的浏览器都支持 event 对象,但是支持的方式不同。

4.1 获取事件对象

支持 W3C DOM2 级规范的浏览器

在遵循 W3C 规范的浏览器中(IE9+、 Firefox、 Safari、 Chrome 和 Opera),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);

IE 浏览器

对于 IE8.0 及其以下版本,DOM0 级指定的事件处理程序必须通过 window.event 获取对象;通过 attachEvent() 方法添加事件时,不仅支持通过 window.event 获取对象,也支持事件对象作为参数传入句柄。

var btn = document.getElementById("myBtn");
btn.onclick = function(){
var event = window.event;
alert(event.type); //"click"
};
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(){
var event = window.event;
alert(event.type); //"click"
});
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event){
alert(event.type); //"click"
});

BTW

  1. 无论在什么浏览器中,在通过 HTML 特性指定事件处理程序时,变量 event 中保存了 event 对象,如下:

    <input type = "button" value = "Click me" onclick = "alert(event.type)" />
  2. 经过测试后发现,现代浏览器中 IE9+/Opera/Safari/Chrome 不仅可以将事件对象作为参数传入句柄中,而且也能采用 IE 的方法,不传参,直接通过 window 的 event 属性获取。而Firefox 只能将事件对象作为参数传入句柄。

总结

  1. HTML 事件处理程序中通过变量 event 获取 event 对象
  2. 现代浏览器中 IE9+/Opera/Safari/Chrome 既可以通过传参又可以通过 window.event 获取 event 对象; Firefox 只能通过传参获得。
  3. 低版本的 IE 中(8及以下),支持通过 window.event 获取 event 对象,但是只能通过这个途径获取 event 对象的只有 DOM0 级方法绑定的事件处理程序。attachEvent 方法还可以通过传参获得。

4.2 事件对象的属性和方法

event 对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都有下列属性和方法:

W3C 标准事件对象

属性/方法 类型 读/写 说明
type String 只读 被触发的事件的类型
eventPhase Int 只读 1表示捕获阶段,2表示目标阶段,3表示冒泡阶段
target Element 只读 事件的实际目标
currentTarget Element 只读 事件处理程序被绑定那个元素,(在事件处理程序内部,this 始终等于 currentTarget 的值
bubbles Boolean 只读 表示事件是否冒泡
stopPropagation() Function 只读 取消事件的进一步捕获或冒泡。(如果 event.bubbles === true ,则可以使用这个方法)
cancelable Boolean 只读 表示是否可以取消事件的默认行为
preventDefault() Function 只读 取消事件的默认行为,如果 event.cancelable === true ,则可以使用这个方法)
defaultPrevented Boolean 只读 为 true 表示已经调用了 preventDefault() (DOM3)

IE 事件对象

属性/方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认值为 false,但将其设置为 true 就可以取消事件冒泡(与 W3C 标准中的 stopPropagation() 方法的作用相同
returnValue Boolean 读/写 默认值为 true,但将其设置为 false 就可以取消事件的默认行为(与 W3C 标准中的 preventDefault() 方法的作用相同
srcElement Element 只读 事件的目标(与 DOM 中的 target 属性相同
type String 只读 被触发的事件的类型

4.3 跨浏览器的事件对象

IE 中 event 对象的全部信息和方法 DOM 对象中都有,只不过实现方法不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。
可以对前面介绍的 EventUtil 对象进行增强:

var EventUtil = {
addHandler: function(elem, type, handler) = {
if (elem.addEventListener) {
elem.addEventListener(type, handler);
} else if (elem.attachEvent) {
elem.attachEvent("on" + type, handler);
} else {
elem["on" + type] = handler;
}
},
removeHandler: function(elem, type, handler) {
if (elem.removeEventListener) {
elem.removeEventListener(type, handler);
} else if (elem.detachEvent) {
elem.detachEvent("on" + type, handler);
} else {
elem["on" + type] = null;
}
},
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;
}
},
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};