# 事件
JavaScript 与 HTML 的交互是通过事件实现的,事件代表文档或浏览器窗口中某个有意义的时刻。
# 1、事件流
事件流描述了页面接收事件的顺序。
IE 将支持事件冒泡流,而 Netscape Communicator 将支持事件捕获流。
# 1.1 事件冒泡
IE 事件流被称为事件冒泡,这是因为事件被定义为从最具体的元素(文档树中最深的节点)开始触发,然后向上传播至没有那么具体的元素(文档)。
<!DOCTYPE html>
<html>
<head>
<title>Event Bubbling Example</title>
</head>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
2
3
4
5
6
7
8
9
在点击页面中的<div>
元素后,click 事件会以如下顺序发生:
- (1)
<div>
- (2)
<body>
- (3)
<html>
- (4)
document
所有现代浏览器都支持事件冒泡,只是在实现方式上会有一些变化。
# 1.2 事件捕获
Netscape Communicator 团队提出了另一种名为事件捕获的事件流。事件捕获的意思是最不具体的节点应该最先收到事件,而最具体的节点应该最后收到事件。事件捕获实际上是为了在事件到达最终目标前拦截事件。如果前面的例子使用事件捕获,则点击<div>
元素会以下列顺序触发 click 事件:
- (1)
document
- (2)
<html>
- (3)
<body>
- (4)
<div>
虽然这是 Netscape Communicator 唯一的事件流模型,但事件捕获得到了所有现代浏览器的支持。
实际上,所有浏览器都是从 window 对象开始捕获事件,而 DOM2 Events规范规定的是从 document 开始。
# 1.3 DOM 事件流
DOM2 Events 规范规定事件流分为 3 个阶段:事件捕获、到达目标和事件冒泡。事件捕获最先发生,为提前拦截事件提供了可能。然后,实际的目标元素接收到事件。最后一个阶段是冒泡,最迟要在这个阶段响应事件。
在 DOM 事件流中,实际的目标(<div>
元素)在捕获阶段不会接收到事件。这是因为捕获阶段从document 到<html>
再到<body>
就结束了。下一阶段,即会在<div>
元素上触发事件的“到达目标”阶段,通常在事件处理时被认为是冒泡阶段的一部分(稍后讨论)。然后,冒泡阶段开始,事件反向传播至文档。
# 2、事件处理程序
事件意味着用户或浏览器执行的某种动作。比如,单击(click)、加载(load)、鼠标悬停(mouseover)。为响应事件而调用的函数被称为事件处理程序(或事件监听器)。事件处理程序的名字以"on"开头,因此 click 事件的处理程序叫作 onclick,而 load 事件的处理程序叫作 onload。
# 2.1 HTML 事件处理程序
<input type="button" value="Click Me" onclick="console.log('Clicked')"/>
# 2.2 DOM事件级别
DOM 级别一共可以分为四个级别:DOM0 级、DOM1 级、DOM2 级和 DOM3 级。而 DOM 事件分为 3 个级别:DOM 0 级事件处理,DOM 2 级事件处理和 DOM 3 级事件处理。由于 DOM 1 级中没有事件的相关内容,所以没有DOM 1 级事件。
(1)DOM0 事件处理程序
每个元素(包括 window 和 document)都有通常小写的事件处理程序属性,比如 onclick。只要把这个属性赋值为一个函数即可:
let btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log("Clicked");
console.log(this.id); // "myBtn"
};
btn.onclick = null; // 移除事件处理程序
2
3
4
5
6
(2)DOM2 事件处理程序
DOM2 Events 为事件处理程序的赋值和移除定义了两个方法:addEventListener() 和 removeEventListener()。
这两个方法暴露在所有 DOM 节点上,它们接收 3 个参数:事件名、事件处理函数和一个布尔值,true 表示在捕获阶段调用事件处理程序,false(默认值)表示在冒泡阶段调用事件处理程序。
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
2
3
4
添加了两个事件处理程序。多个事件处理程序以添加顺序来触发。
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
// 其他代码
btn.removeEventListener("click", function() { // 没有效果!
console.log(this.id);
}, false);
// 绑定 handler
let btn = document.getElementById("myBtn");
let handler = function() {
console.log(this.id);
};
btn.addEventListener("click", handler, false);
// 其他代码
btn.removeEventListener("click", handler, false); // 有效果!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
addEventListener() 和 removeEventListener() 必须是传入同一个函数才有效。
# 3、事件对象
在 DOM 中发生事件时,所有相关信息都会被收集并存储在一个名为 event 的对象中。这个对象包含了一些基本信息,比如导致事件的元素、发生的事件类型,以及可能与特定事件相关的任何其他数据。
DOM 合规的浏览器中,event 对象是传给事件处理程序的唯一参数。
不管以哪种方式(DOM0 或 DOM2)指定事件处理程序,都会传入这个 event 对象。
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.type); // "click"
};
btn.addEventListener("click", (event) => {
console.log(event.type); // "click"
}, false);
2
3
4
5
6
7
所有事件对象都会包含下表列出的这些公共属性和方法。
属性/方法 | 类 型 | 读/写 | 说 明 |
---|---|---|---|
target | 元素 | 只读 | 事件目标 |
currentTarget | 元素 | 只读 | 当前事件处理程序所在的元素 |
preventDefault() | 函数 | 只读 | 用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法 |
defaultPrevented | 布尔值 | 只读 | true 表示已经调用 preventDefault()方法(DOM3 Events 中新增) |
bubbles | 布尔值 | 只读 | 表示事件是否冒泡 |
stopPropagation() | 函数 | 只读 | 用于取消所有后续事件捕获或事件冒泡。只有 bubbles 为 true 才可以调用这个方法 |
stopImmediatePropagation() | 函数 | 只读 | 用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序(DOM3 Events 中新增) |
cancelable | 布尔值 | 只读 | 表示是否可以取消事件的默认行为 |
detail | 整数 | 只读 | 事件相关的其他信息 |
eventPhase | 整数 | 只读 | 表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段 |
trusted | 布尔值 | 只读 | true 表示事件是由浏览器生成的。false 表示事件是开发者通过 JavaScript 创建的(DOM3 Events 中新增) |
type | 字符串 | 只读 | 被触发的事件类型 |
View | AbstractView | 只读 | 与事件相关的抽象视图。等于事件所发生的 window 对象 |
在事件处理程序内部,this 对象始终等于 currentTarget 的值,而 target 只包含事件的实际目标。如果事件处理程序直接添加在了意图的目标,则 this、currentTarget 和 target 的值是一样的。
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.currentTarget === this); // true
console.log(event.target === this); // true
};
2
3
4
5
event 对象只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。
# 4、事件类型
DOM3 Events 定义了如下事件类型。
- 用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。
- 焦点事件(FocusEvent):在元素获得和失去焦点时触发。
- 鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。
- 滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。
- 输入事件(InputEvent):向文档中输入文本时触发。
- 键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。
- 合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发。
除了这些事件类型之外,HTML5 还定义了另一组事件,而浏览器通常在 DOM 和 BOM 上实现专有事件。这些专有事件基本上都是根据开发者需求而不是按照规范增加的,因此不同浏览器的实现可能不同。
# 4.1 用户界面事件
用户界面事件或 UI 事件不一定跟用户操作有关。这类事件在 DOM 规范出现之前就已经以某种形式存在了,保留它们是为了向后兼容。
- load:在 window 上当页面加载完成后触发,在窗套(
<frameset>
)上当所有窗格(<frame>
)都加载完成后触发,在<img>
元素上当图片加载完成后触发,在<object>
元素上当相应对象加载完成后触发。 - unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在
<object>
元素上当相应对象卸载完成后触发。 - abort:在
<object>
元素上当相应对象加载完成前被用户提前终止下载时触发。 - error:在 window 上当 JavaScript 报错时触发,在
<img>
元素上当无法加载指定图片时触发,在<object>
元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时触发。 - select:在文本框(
<input>
或 textarea)上当用户选择了一个或多个字符时触发。 - resize:在 window 或窗格上当窗口或窗格被缩放时触发。
- scroll:当用户滚动包含滚动条的元素时在元素上触发。
<body>
元素包含已加载页面的滚动条。
# 4.2 焦点事件
焦点事件在页面元素获得或失去焦点时触发。这些事件可以与 document.hasFocus() 和 document.activeElement 一起为开发者提供用户在页面中导航的信息。
- blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
- focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
- focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。
- focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。
当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件。
- (1) focuscout 在失去焦点的元素上触发。
- (2) focusin 在获得焦点的元素上触发。
- (3) blur 在失去焦点的元素上触发。
- (4) DOMFocusOut 在失去焦点的元素上触发。
- (5) focus 在获得焦点的元素上触发。
- (6) DOMFocusIn 在获得焦点的元素上触发。
# 4.3 鼠标和滚轮事件
DOM3 Events 定义了 9 种鼠标事件。
- click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。
- dblclick:在用户双击鼠标主键(通常是左键)时触发。
- mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
- mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
- mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。
- mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。
- mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
- mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
- mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。
# 4.4 事件坐标
event.clientX
客户端 x 坐标event.clientY
客户端 y 坐标event.pageX
页面 x 坐标event.pageY
页面 y 坐标event.screenX
屏幕 x 坐标event.screenY
屏幕 y 坐标
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`);
console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`);
console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`);
});
2
3
4
5
6
# 4.5 修饰键
DOM 规定了 4 个属性来表示这修饰键的状态:shiftKey、ctrlKey、altKey 和 metaKey。
let div = document.getElementById("myDiv");
div.addEventListener("click", (event) => {
let keys = new Array();
if (event.shiftKey) {
console.log("shift");
}
if (event.ctrlKey) {
console.log("ctrl");
}
if (event.altKey) {
console.log("alt");
}
if (event.metaKey) {
console.log("meta");
}
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 4.6 鼠标按键
- 0,表示没有按下任何键;
- 1,表示按下鼠标主键;
- 2,表示按下鼠标副键;
- 3,表示同时按下鼠标主键、副键;
- 4,表示按下鼠标中键;
- 5,表示同时按下鼠标主键和中键;
- 6,表示同时按下鼠标副键和中键;
- 7,表示同时按下 3 个键。
# 4.7 键盘与输入事件
键盘事件包含 3 个事件:
- keydown,用户按下键盘上某个键时触发,而且持续按住会重复触发。
- keyup,用户释放键盘上某个键时触发。
- textInput,在字符被输入到可编辑区域时触发。
let textbox = document.getElementById("myText");
textbox.addEventListener("keyup", (event) => {
console.log(event.keyCode);
});
2
3
4
# 4.8 合成事件
合成事件是 DOM3 Events 中新增的,用于处理通常使用 IME 输入时的复杂输入序列。IME 可以让用户输入物理键盘上没有的字符。
IME 通常需要同时按下多个键才能输入一个字符。合成事件用于检测和控制这种输入。
合成事件有以下 3 种:
- compositionstart,在 IME 的文本合成系统打开时触发,表示输入即将开始;
- compositionupdate,在新字符插入输入字段时触发;
- compositionend,在 IME 的文本合成系统关闭时触发,表示恢复正常键盘输入。
# 4.9 变化事件
DOM2 的变化事件(Mutation Events)是为了在 DOM 发生变化时提供通知。
变化事件已经被Mutation Observers 所取代,可以参考 MutationObserver 接口。
# 4.10 HTML5 事件
- contextmenu 事件,可以处理页面上的所有同类事件。
- beforeunload 事件,在页面即将从浏览器中卸载时触发
- DOMContentLoaded 事件,在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript 文件、CSS 文件或其他资源加载完成。
# 4.11 设备事件
设备事件可以用于确定用户使用设备的方式。
(1)orientationchange 事件
判断用户的设备是处于垂直模式还是水平模式。
移动 Safari 在 window 上暴露了 window.orientation 属性,它有以下 3 种值之一:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主屏幕键在左)。
所有 iOS 设备都支持 orientationchange 事件和 window.orientation 属性。
(2)deviceorientation 事件
可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在 window 上触发。
设备本身处于 3D 空间即拥有 x 轴、y 轴和 z 轴的坐标系中。如果把设备静止放在水平的表面上,那么三轴的值均为 0,其中,x 轴方向为从设备左侧到右侧,y 轴方向为从设备底部到上部,z 轴方向为从设备背面到正面
当 deviceorientation 触发时,event 对象中会包含各个轴相对于设备静置时坐标值的变化,主要是以下 5 个属性。
- alpha:0~360 范围内的浮点值,表示围绕 z 轴旋转时 y 轴的度数(左右转)。
- beta:–180~180 范围内的浮点值,表示围绕 x 轴旋转时 z 轴的度数(前后转)。
- gamma:–90~90 范围内的浮点值,表示围绕 y 轴旋转时 z 轴的度数(扭转)。
- absolute:布尔值,表示设备是否返回绝对值。
- compassCalibrated:布尔值,表示设备的指南针是否正确校准。
(3)devicemotion 事件
用于提示设备实际上在移动,而不仅仅是改变了朝向。
- acceleration:对象,包含 x、y 和 z 属性,反映不考虑重力情况下各个维度的加速信息。
- accelerationIncludingGravity:对象,包含 x、y 和 z 属性,反映各个维度的加速信息,包含 z 轴自然重力加速度。
- interval:毫秒,距离下次触发 devicemotion 事件的时间。此值在事件之间应为常量。
- rotationRate:对象,包含 alpha、beta 和 gamma 属性,表示设备朝向。
# 4.12 触摸及手势事件
Safari 为 iOS 定制了一些专有事件,以方便开发者。因为 iOS 设备没有鼠标和键盘,所以常规的鼠标和键盘事件不足以创建具有完整交互能力的网页。同时,WebKit 也为 Android 定制了很多专有事件,成为了事实标准,并被纳入 W3C 的 Touch Events 规范。
触摸事件有如下几种。
- touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
- touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。
- touchend:手指从屏幕上移开时触发。
- touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。
这些事件都会冒泡,也都可以被取消。
手势事件:在两个手指触碰屏幕且相对距离或旋转角度变化时触发。
- gesturestart:一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。
- gesturechange:任何一个手指在屏幕上的位置发生变化时触发。
- gestureend:其中一个手指离开屏幕时触发。
# 5、内存与性能
“过多事件处理程序”的解决方案是使用事件委托。事件委托利用事件冒泡,可以只使用一个事件处理程序来管理一种类型的事件。
比如有以下 HTML:
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
2
3
4
5
绑定事件:
let item1 = document.getElementById("goSomewhere");
let item2 = document.getElementById("doSomething");
let item3 = document.getElementById("sayHi");
item1.addEventListener("click", (event) => {
location.href = "http:// www.wrox.com";
});
item2.addEventListener("click", (event) => {
document.title = "I changed the document's title";
});
item3.addEventListener("click", (event) => {
console.log("hi");
});
// 修改过后
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
}
});
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
最适合使用事件委托的事件包括:click、mousedown、mouseup、keydown 和 keypress。
# 6、模拟事件
# 6.1 DOM 事件模拟
任何时候,都可以使用 document.createEvent() 方法创建一个 event 对象。这个方法接收一个参数,此参数是一个表示要创建事件类型的字符串。
- "UIEvents"(DOM3 中是"UIEvent"):通用用户界面事件(鼠标事件和键盘事件都继承自这个事件)。
- "MouseEvents"(DOM3 中是"MouseEvent"):通用鼠标事件。
- "HTMLEvents"(DOM3 中没有):通用 HTML 事件(HTML 事件已经分散到了其他事件大类中)。
创建 event 对象之后,需要使用事件相关的信息来初始化。事件模拟的最后一步是触发事件。为此要使用 dispatchEvent() 方法
(1)模拟鼠标事件
initMouseEvent() 方法,用于为新对象指定鼠标的特定信息。
initMouseEvent() 方法接收 15 个参数,分别对应鼠标事件会暴露的属性。这些参数列举如下。
- type(字符串):要触发的事件类型,如"click"。
- bubbles(布尔值):表示事件是否冒泡。为精确模拟鼠标事件,应该设置为 true。
- cancelable(布尔值):表示事件是否可以取消。为精确模拟鼠标事件,应该设置为 true。
- view(AbstractView):与事件关联的视图。基本上始终是 document.defaultView。
- detail(整数):关于事件的额外信息。只被事件处理程序使用,通常为 0。
- screenX(整数):事件相对于屏幕的 x 坐标。
- screenY(整数):事件相对于屏幕的 y 坐标。
- clientX(整数):事件相对于视口的 x 坐标。
- clientY(整数):事件相对于视口的 y 坐标。
- ctrlkey(布尔值):表示是否按下了 Ctrl 键。默认为 false。
- altkey(布尔值):表示是否按下了 Alt 键。默认为 false。
- shiftkey(布尔值):表示是否按下了 Shift 键。默认为 false。
- metakey(布尔值):表示是否按下了 Meta 键。默认为 false。
- button(整数):表示按下了哪个按钮。默认为 0。
- relatedTarget(对象):与事件相关的对象。只在模拟 mouseover 和 mouseout 时使用。
模拟单击:
let btn = document.getElementById("myBtn");
// 创建 event 对象
let event = document.createEvent("MouseEvents");
// 初始化 event 对象
event.initMouseEvent("click", true, true, document.defaultView,
0, 0, 0, 0, 0, false, false, false, false, 0, null);
// 触发事件
btn.dispatchEvent(event);
2
3
4
5
6
7
8
(2)模拟键盘事件
在 DOM3 中创建键盘事件的方式是给 createEvent() 方法传入参数"KeyboardEvent"。这样会返回一个 event 对象,这个对象有一个 initKeyboardEvent() 方法。这个方法接收以下参数。
- type(字符串):要触发的事件类型,如"keydown"。
- bubbles(布尔值):表示事件是否冒泡。为精确模拟键盘事件,应该设置为 true。
- cancelable(布尔值):表示事件是否可以取消。为精确模拟键盘事件,应该设置为 true。
- view(AbstractView):与事件关联的视图。基本上始终是 document.defaultView。
- key(字符串):按下按键的字符串代码。
- location(整数):按下按键的位置。0 表示默认键,1 表示左边,2 表示右边,3 表示数字键盘,4 表示移动设备(虚拟键盘),5 表示游戏手柄。
- modifiers(字符串):空格分隔的修饰键列表,如"Shift"。
- repeat(整数):连续按了这个键多少次。
注意,DOM3 Events 废弃了 keypress 事件,因此只能通过上述方式模拟 keydown 和 keyup 事件
let textbox = document.getElementById("myTextbox"),
event;
// 按照 DOM3 的方式创建 event 对象
if (document.implementation.hasFeature("KeyboardEvents", "3.0")) {
event = document.createEvent("KeyboardEvent");
// 初始化 event 对象
event.initKeyboardEvent("keydown", true, true, document.defaultView, "a",
0, "Shift", 0);
}
// 触发事件
textbox.dispatchEvent(event);
2
3
4
5
6
7
8
9
10
11
(3)模拟其他事件
let event = document.createEvent("HTMLEvents");
event.initEvent("focus", true, false);
target.dispatchEvent(event);
2
3
(4)自定义 DOM 事件
要创建自定义事件,需要调用 createEvent("CustomEvent") 。返回的对象包含 initCustomEvent() 方法,该方法接收以下 4 个参数。
- type(字符串):要触发的事件类型,如"myevent"。
- bubbles(布尔值):表示事件是否冒泡。
- cancelable(布尔值):表示事件是否可以取消。
- detail(对象):任意值。作为 event 对象的 detail 属性。
let div = document.getElementById("myDiv"),
event;
div.addEventListener("myevent", (event) => {
console.log("DIV: " + event.detail);
});
document.addEventListener("myevent", (event) => {
console.log("DOCUMENT: " + event.detail);
});
if (document.implementation.hasFeature("CustomEvents", "3.0")) {
event = document.createEvent("CustomEvent");
event.initCustomEvent("myevent", true, false, "Hello world!");
div.dispatchEvent(event);
}
2
3
4
5
6
7
8
9
10
11
12
13