本篇内容
  • 事件流
  • 事件处理程序
  • 事件对象

JavaScript与HTML之间的交互是通过事件实现的。
事件,就是捕或浏览器窗口中发生的一些特定的交互瞬间。可以使用侦听器(或处理程序)来预订事件,以便事件发生时执行相应的代码。

这种在传统软件工程中被称为观察员模式的模型,支持页面的行为(JavaScript代码)与页面的外观(HTML和css代码)之间的松散耦合。

事件最早是在IE3和Netscape Navigator 2中出现的,IE8是最后一个仍然使用其专有事件系统的主要浏览器。

1 事件流

事件流描述的是从页面中接收事件的顺序。IE的事件流是事件冒泡流,而NetscapeCommunicator的事件流是事件捕获流。

1.1 事件冒泡

IE的事件流叫做事件冒泡(event bubbling),即事件开始时由最具体的元素(文挡中嵌套层次最深的那个节点)接收,然后逐级向上传播到较为不具体的节点(文档)。


1
2
3
4
5
6
7
8
9
<!Doctype html>
<html>
<head>
<title>test</title>
</head>
<body>
<div id="testDiv">click</div>
</body>
</html>

如果点击了页面中的<div>元素,那么这个click事件会按照如下顺序传播:

  • 1.<div>
  • 2.<body>
  • 3.<html>
  • 4.document

所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。IE5.5及更阜版本中的事件冒泡会跳过<html>元素(从<body>直接跳到document)。IE9、Firefox、Chrome和Safari则将事件一直冒泡到window对象。

1.2 事件捕获

Netscape Communicator团队提出的另一种事件流叫做事件捕获(event capturing)。事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。事件捕获的用意在于在事件到达预定目标之前捕获它。如果仍以前面的HTML页面作为演示事件捕获的例子,那么单击<div>元素就会以下列顺序触发click事件。

  • 1.document
  • 2.<html>
  • 3.<body>
  • 4.<div>

在事件捕获过程中,document对象首先接收到 click事件,然后事件沿DOM树依次向下,一直传播到事件的实际目标,自到<div>元素。

虽然事件捕我是 Netscape Communicator唯一支持的事件流模型,但IE9、Safari、Chrome、Opera和Firefox目前也都支持这种事件流模型。尽管“DOM2级事件”规范要求事件应该从document对象开始传播,但这些浏览器都是从window对象开始捕获事件的。
由于老版本的浏览器不支持,因此很少有人使用事件捕获。

1.3 对事件冒泡和捕捉的解释

当一个事件发生在具有父元素的元素上(例如,在我们的例子中是<video>元素)时,现代浏览器运行两个不同的阶段 - 捕获阶段和冒泡阶段。 在捕获阶段:

  • 浏览器检查元素的最外层祖先<html>,是否在捕获阶段中注册了一个onclick事件处理程序,如果是,则运行它。
  • 然后,它移动到<html>中的下一个元素,并执行相同的操作,然后是下一个元素,依此类推,直到到达实际点击的元素。

在冒泡阶段,恰恰相反:

  • 浏览器检查实际点击的元素是否在冒泡阶段中注册了一个onclick事件处理程序,如果是,则运行它;
  • 然后它移动到下一个直接的祖先元素,并做同样的事情,然后是下一个,等等,直到它到达<html>元素。

事件捕获和冒泡

1.3 DOM事件流

“DOM2级事件” 规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。
首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。

以前面简单的 HTML 页图为例,
事件流

在DOM事件流中,实际的目标(<div>元素)在捕获阶段不会接收到事件。这意味着在铺在阶段,事件从document到<html>再到<body>后就停止了。下一个阶段是“处于目标”阶段,于是事件在<div>上发生 并在事件处理(后面将会讨论这个概念)中被看成冒泡阶段的一部分。然后,冒泡阶段发生 事件又传播回文挡。

多数支持DOM事件流的浏览器都实现了一种恃定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。

IE9、Opera、Firefox、Chrome和Safari都支持DOM事件流;IE8及更早版本不支持DOM事件流。

2 事件处理程序

事件就是用户或浏览楼自身执行的某种动作。诸如click、load和mouseover,都是事件的名字。而响应某个事件的函数就叫做事件处理程序(或事件侦昕器)。事件处理程序的名字以”on”开头,因此 click事件的事件处理程序就是onclick,load事件的事件处理程序就是onload。为事件指定处理程序的方式有好几种。

2.1 HTML事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的HTML特性来指定。这个特性的值应该是能够执行的JavaScript代码。

例如,要在按钮被单击时执行一些JavaScript

1
<input type="button" value="click me" onclick="alert('clicked')" />

这个操作是通过指定onclick特性并将一些JavaScript代码作为它的值来定义的。由于这个值是JavaScript,因此不能在其中使用未经转义的HTML语法字符,例如和号(&)、双引号(””)、小于号(<)或大于号(>)。为了避免使用HTML实体,这里使用了单引号。

如果想要使用双引号

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

事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。
这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象。通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this值等于事件的目标元素。


1
<input type="button" value="click me" onclick="alert(this.value)" />

关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。这个函数使用with像下面这样扩展作用域:

1
2
3
4
5
6
7
function () {
width (document) {
width (this) {
// 元素属性值
}
}
}

如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前丽的例子效果相同:

1
<input type="button" value="click me" onclick="alert(value)" />

不过,在HTML制定时间处理程序有两个缺点。首先,存在一个时差问题,因为用户可能会在HTML元素已出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。为此,很多HTML事件处理程序都会被封装在一个try-catch块中.以便错误不会浮出水面

1
<input type="button" value="click me" onclick="try{showMessage();}catch(e){}" />

另一个缺点是,这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同JavaScript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。

通过HTML指定事件处理程序的最后一个缺点是HTML与JavaScript代码紧密藕合。如果要更换事件处理程序,就要改动两个地方:HTML代码和JavaScript代码。而这正是许多开发人员摒弃HTML事件处理程序,转而使用JavaScript指定事件处理程序的原因所在。

2.2 DOM 0级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是将一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因一是简单,二是具有跨浏览器的优势。要使用JavaScript指定事件处理程序,首先必须取得一个要操作的对象的引用。
每个元素(包括window和document)都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设建为一个函数。就可以指定事件处理程序,如下

1
2
3
4
var btn = document.getElementById('myBtn');
btn.onclick = function () {
alert('clicked');
};

在此,我们通过文档对象取得了一个按钮的引用,然后为它指定了onclick事件处理程序。但要注意,在这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段时间内怎么单击都没有反应。
使用DOMO级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域巾运行;换句话说,程序中的this引用当前元素


1
2
3
4
var btn = document.getElementById('myBtn');
btn.onclick = function () {
alert(this.id);
};

以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。

也可以删除通过DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为null即可:

1
btn.onclick = null;

将事件处理程序设置为null之后,再单击按钮将不会有任何动作发生。

如泉你使用HTML指定事件处理程序,那么onclick属性的值就是一个包含着在同名HTML特性中指定的代码的函数。而将相应的属性设置为null,也可以删除以这种方式指定的事件处理程序。

2.3 DOM2 级事件处理程序

“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接受3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。


1
2
3
btn.addEventListener('click', function () {
alert(this.id);
}, false)

使用 DOM2 级方法添加事件处理程序的主要好处是可以添加多个事件处理程序。

1
2
3
4
5
6
btn.addEventListener('click', function () {
alert(this.id);
}, false);
btn.addEventListener('click', function () {
alert(this.innerHTML);
}, false);

这里为按钮添加了两个事件处理程序。这两个事件处理程序会按照添加它们的顺序触发。

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除。

1
2
3
4
5
6
function click () {
alert(this.id)
}

btn.addEventListener('click', click, false);
btn.removeEventListener('click', click, false);

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

DOM2兼容:IE9、Firefox、Safari、Chrome和Opera。

2.4 IE 事件处理程序

IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接受相同的两个参数:事件处理程序名称与事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。


1
2
3
4
// 注意第一个参数的不同
btn.attachEvent('onclick', function () {
alert('test');
});

在IE中使用attachEvent()与使用DOMO级方法的主要区别在于事件处理程序的作用域。在使 用DOM0级方法的情况下,事件处理程序会在其所属元素的作用域内运行;在使用attachEvent()方法的情况下,事件处理程序会在全局作用域中运行,因此this等于window。

1
2
3
btn.attachEvent('onclick', function () {
alert(this === window); // true
});

与addEventListener()类似,attachEvent()方法也可以用来为一个元素添加多个事件处理程序。

1
2
3
4
5
6
btn.attachEvent('onclick', function () {
alert('test1');
});
btn.attachEvent('onclick', function () {
alert('test2');
});

不过,与DOM方法不同的是,这些事件处理程序不是以添加它们的顺序执行,而是以相反的顺序被触发。
使用attachEvent()添加的事件可以通过detachEvent()来移除,条件是必须提供相同的参数。与DOM 方法一样,这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给detachEvent(),就可以移除相应的事件处理程序。

1
2
3
4
5
6
function click () {
alert('test')
}

btn.attachEvent('onclick', click);
btn.detachEvent('onclick', click);

支持IE事件处理程序的浏览器只有IE和Opera。

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

要保证处理事件的代码能在大多数浏览器下一致地运行.只需关注冒泡阶段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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;
}
}
};

没有考虑到所有的调览器问题,例如在IE中的作用域问题。不过,使用它们添加利移除事件处理程序还是足够了。此外还要注意,DOM0级对每个事件只支持一个事件处理程序。

3 事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包指导致事件的元素、事件的类型以及其他与特定事件相关的信息。例如,鼠标操作导致的事件 对象中,会包含鼠标位置的信息,而键盘操作导效的事件对象中,会包含与按下的键有关的信息。所有 浏览器都支将event对象,但支持方式不同。

mdn-Event

3.1 DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOMO级或DOM2级),都会传入event对象。


1
2
3
4
5
6
7
btn.onclick = function (event) {
alert(event.type); // 'click'
};

btn.addEventListener('click', function (event) {
alert(event.type); // 'click'
}, false);

在通过HTML待性指定事件处理程序时,变量event中保存着event对象。如

1
<input type="button" value="click me" onclick="try{alert(event.type)}catch(e){}" />

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

属性/方法 类型 读/写 说明
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对象

在事件处理程序内部,对象this始终等于currentTarget的值,而target则只包含事件的实际目标。如果直接将事件处理程序指定给了目标元素,则this、currentTarget和target包含相同的值。
如:

1
2
3
4
btn.onclick = function (event) {
alert(this === event.currentTarget); // true
alert(this === event.target); // true
}

此例中由于click事件的目标是按钮,因此这三个值是相等的。如果事件处理程序存在于按钮的父节点中(例如document.body),那么这些值是不相同的。

1
2
3
4
5
document.body.onclick = function (event) {
alert(this === event.currentTarget); // true
alert(this === event.target); // false
alert(event.target === document.getElementById('btn')); // true
}

要阻止特定事件的默认行为,可以使用preventDefault()方法。例如,链接的默认行为就是在被且在击时会导航到其href特性指定的URL。如果你想阻止链接导航这一默认行为,那么通过链接的onclick事件处理程序可以取消它,如

1
2
3
4
var link = docuemnt.getElementById('link');
link.onclick = function (event) {
event.preventDefault();
}

只有cancelable属性设置为true的事件,才可以使用preventDefault()来取消其默认行为。
另外,stopPropagation()方法用于立即停止事件在DOM层次中的传播,即取消进一步的事件 捕获或冒泡。例如,直接添加到一个按钮的事件处理程序可以调用stopPropagation(),从而避免触发注册在document.body上丽的事件处理程序,如

1
2
3
4
5
6
7
btn.onclick = function (event) {
alert('btn click');
event.stopPropagation();
};
document.body.onclick = function () {
alert('body click');
}

事件对象的eventPhase属性,可以用来确定事件当前正位于事件流的哪个阶段。如果是在捕获阶段调用的事件处理程序,那么eventPhase等于1;如果事件处理程序处于目标对象上,则eventPhase等于2;如果是在冒泡阶段调用的事件处理理序,eventPhase等于3。这里要注意的是,尽管 “处于目标 ” 发生在冒泡阶段,但eventPhase仍然一直等于2。


1
2
3
4
5
6
7
8
9
btn.onclick = function (event) {
alert(event.eventPhase); //2
};
docuemnt.body.addEventListener('click', function (event) {
alert(event.eventPhase); // 1
}, true);
document.body.onclick = function (event) {
alert(event.eventPhase); // 3
}

只有在事件处理时执行期间,event对象才会存在:一旦事件处理程序执行完,event对象会立即销毁。

3.2 IE 中的事件对象

与访问 DOM 中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用 DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。

1
2
3
btn.onclick = function () {
alert(window.event.type); // 'click'
}

如果事件处理程序是使用attachEvent()添加的, 那 么就会有一个event对象作为参数被传人事件处理程序函数中,如

1
2
3
btn.attachEvent('onclick', function (event) {
alert(event.type); // 'click'
})

在像这样使用attachEvent()的情况下,也可以通过window对象来访问event对象,就像使用DOM0级方法时一样。不过为方便起见,同一个对象也会作为参数传递。

IE的event对象同样也 包含与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的DOM属性和方法。与DOM的event对象一样,这些属性和方法也会因为事件类型的不同而不同,但所有事件对象都会包含下表所列的属性和方法。

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

因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于事件目标。故而,最好还是使用event.srcElement比较保险。

1
2
3
4
5
6
btn.onclick = function () {
alert(window.event.srcElement === this); // true
};
btn.attachEvent('onclick', function (event) {
alert(evetn.srcElement === this); // false
})

returnValue属性相当于DOM中的preventDefault()方法, 它们的作用都是取消给定事件的默认行为。只要将returnvalue设置为false,就可以阻止默认行为。

1
2
3
link.onclick = function () {
window.event.returnvalue = false;
}

与DOM不同的是,在此没有办法确定事件是有能被取消。

相应地,cancelBubble属性与DOM中的stopPropagation()方法作用相同,都是用来停止事件冒泡的。由于IE不支持事件捕获,因而只能取消事件冒泡;但stopPropagatioin()可以同时取消事件捕获和冒泡。

1
2
3
4
5
6
7
btn.onclick = function (event) {
alert('btn click');
window.event.cancelBubble = true;
};
document.body.onclick = function () {
alert('body click');
}

通过在onclick事件处理程序中将cancelBubble设置为true,就可阻止事件通过冒泡而触发dcument.body中注册的事件处理程序。结果.在单击按钮之后,只会显示一个警告框。

3.3 跨浏览器的事件对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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;
}
}
}

温习:

  • 事件冒泡、事件捕获、DOM事件流
  • DOM0、DOM2、IE事件处理程序
  • DOM、IE事件对象

(完)