概述
本文中的 DOM ( 大写 ) 一般指 W3C 中的 DOM 标准。
1. 事件冒泡和事件捕获
早期的两家浏览器厂商( IE 和 Netscape Communicator ) 分别使用了两种事件处理机制( 事件冒泡和事件捕获 )。
1.1 事件冒泡
事件开始时由最具体的元素( 文档中嵌套层次最深的节点,也是 DOM 树中的叶子节点 )接收,然后逐级向上传播到较为不具体的节点。如下面的 DOM 结构
<!Document html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>如果单击了 <div id="myDiv"> 这个元素,那么这个事件会按照如下顺序传播:
<div> -> <body> -> <html> -> document -> windowIE8 只会冒泡到 document 。
1.2 事件捕获
和事件冒泡恰恰相反,不太具体的节点会更早地接收到事件,最具体的节点会最后接收到事件。在上面的例子中,事件的传播顺序就会变为:
window -> document -> <html> -> <body> -> <div>值得注意的是,目前的主流浏览器和 IE9+ 都同时支持这两种事件流模型。
1.3 DOM 事件流
“DOM2级事件”标准中规定的事件流应该包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
首先发生的事件捕获,然后是实际的目标接收到事件,最后是冒泡阶段。
在 DOM 事件流,实际的目标( <div> )在捕获阶段不会接收到事件,捕获阶段到 <body> 后就停止。下一阶段 - “处于目标”阶段,事件在 <div> 上发生,并在事件处理中被认为是“冒泡阶段”的一部分。
多数浏览器都可以在捕获阶段和冒泡阶段处理事件。
2. 事件处理程序
2.1 HTML 事件处理程序
HTML 元素支持的每一种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。如下:
<input type="button" value="Click Me" onclick="alert('Clicked')" />
和其他特性不同的是,由于特性值是 javascript,因此不能再其中使用未经转义的 HTML 语法字符,如 & "" < > 等。
<input type="button" value="Click Me" onclick="alert("Clicked")" />
通过这种方式指定事件处理程序有一些独到之处。
- 这样会创建一个封装着元素属值的函数。即
onclick="alert('Clicked')"会创建一个下面的函数function (event) { alert('Clicked'); }在 HTML 行内使用时,不需要定义 Event 就可以使用它。 - 这个函数的扩展作用域很神奇。this 会指向目标元素本身,且可以像访问局部变量一样,访问 document 及该元素本身的成员,也就是说下面两端代码是等同的。
<input type="button" value="Click Me" onclick="alert(this.value)" />
<input type="button" value="Click Me" onclick="alert(value)" />在 HTML 中指定事件处理程序,有如下缺点
- HTML 的解析需要过程,如果事件触发在解析到指定函数之前的话,就会出问题。
- 扩展事件处理程序的作用域链在不同的浏览器中会导致不同的结果
- HTML 和 Javascript 代码过于耦合
2.2 DOM0 级事件处理程序
这是指早期通过 Javascript 指定事件处理程序的方式,将一个函数复制给一个事件处理程序属性。示例如下:
var btn = document.getElementById('myBtn');
btn.onclick = function (e) {
alert(this.id);
};每个元素( 包括 window 和 document )都有自己的事件处理程序属性( 这些属性通常小写 )。
和在 HTML 中定义事件处理程序一样,该函数中的 this 指向元素本身,但不能省略。
关于 DOM0 级事件处理程序需要注意以下几点
- 通过非箭头函数方式定义的方法( 如上述示例代码中方法 ),this 会指向元素本身
- 这种方式定义的事件处理程序,会在事件流的冒泡阶段处理
- 重置或修改 DOM0 级方法指定的事件处理程序,只能通过重置事件处理属性的值来进行,如
btn.onclick = null;
2.3 DOM2 级事件处理程序
DOM2 级事件处理程序定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener() 和 removeEventListener() 。
所有的 DOM 节点都包含这两个方法,且这两个方法都接收 3 个参数,先后是: 要处理的事件名、事件处理函数、事件处理阶段( 布尔值,true 代表在捕获阶段调用事件处理函数,false 代表在冒泡阶段调用事件处理函数 )。实际上,最近两年,最后一个参数已经发生了变化,具体 addEventListener 的现阶段完整 api 介绍,可以参考 MDN 手册:developer.mozilla.org/zh-CN/docs/… 。
通过 addEventListener() 添加的事件处理程序只能通过 removeEventListener() 来移除。这句话的更深层次含义是,通过 addEventListener() 添加的匿名函数将无法被移除。建议使用如下代码来操作:
var btn = document.getElementById('myBtn');
var handler = function () {
alert(this.id);
}
btn.addEventListener('click', handler, false);
btn.removeEventListener('click', handler, false);在大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大程度地兼容各类浏览器( 这也是为什么第三个参数默认值是 false 的原因 )。应该按照如下原则处理:除非必须在事件到达目标之前截获它,否则不应该在事件捕获阶段注册事件处理程序。
2.4 番外: IE9 以下版本的 IE 事件处理程序
IE8 及以下版本不支持 DOM2 级事件处理程序,单独提供了两个类似的方法:attachEvent() 和 detachEvent()。这两个函数只接受两个参数:事件处理程序名称和事件处理程序函数。因为 IE8 及更早版本只支持事件冒泡,所以通过 attachEvent() 添加的事件处理程序都会被添加到冒泡阶段。
var btn = document.getElementById('myBtn');
var handler = function () {
alert(this === window); // true
}
btn.attachEvent('onclick', handler);注意,和 DOM 事件有以下几点不同
- 此时的事件处理名称和 DOM2级不同,需要加上 on 前缀。
- this 不再指向元素本身,而是指向全局作用域
- 为同一个事件注册的多个事件处理函数,执行顺序和注册顺序相反。
3. 事件对象
就是我们常见的 event
3.1 DOM 中的事件对象
兼容 DOM 的浏览器会将一个 event 对象传入到事件处理程序中,无论是使用什么方式注册的 ( DOM0级 或 DOM2级 或 HTML 中指定的 )。
event 对象中包含于此次事件有关的属性和方法。触发的事件类型不同,可用的属性和方法也不一样。具体可以参考 MDN 手册: developer.mozilla.org/zh-CN/docs/… 。
3.1.1 事件处理函数中的 this 和 event.target event.currentTarget
执行以下代码,然后点击 <input>
<body>
<input type="button" id="myBtn">
<body>
<script>
document.body.onclick = function (e) {
console.log(e.currentTarget === document.body); // true
console.log(this === document.body); // true
console.log(e.target === document.getElementById('myBtn')); // true
}
</script>- this 指向当前事件处理程序对应的元素本身
- event.currentTarget 指向事件处理程序正在处理事件的那个元素
- event.target 指向事件真是的目标
3.1.2 event.cancelable 和 event.preventDefault()
只有 cancelable 属性为 true 的事件,才能通过 e.preventDefault() 方法取消其默认行为。
3.1.3 event.stopPropagation() 用于立即停止事件在 DOM 层次中的传播,取消进一步的事件捕获或冒泡( 但是要注意当前事件注册的事件阶段 )。
3.2 番外: IE 中的事件对象
3.2.1 访问 event 对象
和 DOM 中的事件对象不同,要访问 IE 中的 event 对象,有几种不同的方式,这取决于注册事件处理程序的方法。
使用 DOM0 级方法添加事件处理程序时
var btn = document.getElementById('myBtn');
btn.onclick = function () {
var e = window.event;
alert(e.type); // "click"
}使用 attachEvent() 注册的事件处理程序
var btn = document.getElementById('myBtn');
btn.attachEvent('onclick', function (e) {
alert(e.type); // "click"
}3.2.2 IE 中事件处理程序的作用域是根据指定它的方式来的,不能像 DOM 事件一样,认为是事件目标
var btn = document.getElementById('myBtn');
btn.onclick = function() {
alert(window.event.srcElement === this); // true
};
btn.attachEvent('onclick', function(e) {
alert(e.srcElement === this); // false
});3.3 番外: 跨浏览器的事件对象兼容方案
请参考《JavaScript 高级程序设计》 P360
4. 事件类型
详细请参考《JavaScript 高级程序设计》 P362
4.1 几个比较特殊的事件
- blur: 在元素失去焦点时触发。但是这个事件不会冒泡
- focus: 获得焦点;不会冒泡
- focusin: 获得焦点,会冒泡
- focusout: 失去焦点,会冒泡
当焦点从页面的一个元素移动到另一个元素时,会依次触发以下事件:
focusout ( 上一个元素 ) -> focusin ( 下一个元素 ) -> blur ( 上一个元素 ) -> focus ( 下一个元素 )- click: 单击鼠标主键( 一般是左键 )或者回车键触发。这时为了保证易访问性
- dbclick: 不是 DOM2 级事件规范,但是被广泛支持,在 DOM3 中被纳入标准
- mousedown: 单击任意鼠标按钮触发。不能通过键盘触发这个事件
- mouseenter: 鼠标从元素外部首次移动到元素内部时触发。不冒泡,且光标移动到后代元素上不会触发
- mouseleave: 鼠标从元素内部移动到元素外部时触发。不冒泡,且光标移动到后代元素上不会触发
- mousemove: 鼠标在元素中移动式,反复触发
- mouseout: 鼠标从一个元素上方,移动到另一个元素上方时触发。这个“另一个元素”可以位于这个元素的外部,也可以是这个元素的子元素。
- mouseover: 鼠标位于元素外部,然后用户首次将鼠标移入到另一个元素内时触发。
- mouseup: 释放鼠标时触发。
页面上所有元素都支持鼠标事件。除了 mouseenter 和 mouseleave,所有的鼠标事件都会冒泡,也可以被取消。取消鼠标事件将会影响浏览器的默认行为,甚至影响其他事件。(比如取消 mousedown 或者 mouseup 就不会触发 click 事件)
- 键盘和文本事件顺序: keydown( 按下任意键 ) -> keypress( 按下字符键 ) -> keyup
- 键盘和鼠标事件支持修改键。即对于 shift,ctrl,alt,meta(windows: window, mac: cmd),事件对象中存在对应的 shiftKey,ctrlKey,altKey,metaKey 四个布尔值属性,如果在事件触发时,这些键被按下,对应的属性就为 true。
- DOM3 级事件中新增了 textInput 事件,在可编辑区域输入字符时,就会触发。基本可以替代 keyPress 但是只会在可编辑区域触发( keyPress 可以在任意可以获取焦点的地方触发 )
- DOM2 级的变动事件能够在 DOM 中某一部分发生变化时触发。详情可以参考 MDN 手册:developer.mozilla.org/zh-CN/docs/…
- hashchange 事件,url 发生任何变化时触发,这是现代单页面应用的基础之一。
5. 事件优化
5.1 事件委托
由于在指定事件处理程序时,需要访问 DOM,严重影响整个页面的交互就绪时间。因此可以通过事件委托,利用事件冒泡,指定一个尽量高层次的节点 DOM 管理一类事件。
5.2 移除无用的事件处理程序
每当事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的 JavaScript 代码之间就会建立一个连接。连接越多,页面执行就越慢。
6. 模拟事件
- 利用 js 中原生提供的 Element API 直接出发。如 button.click()
- 利用 document.createEvent() 创建一个完整的 event 对象,实现更复杂的功能。详情参考:developer.mozilla.org/zh-CN/docs/…
- 自定义事件:通过 document.createEvent('CustomEvent') 创建。