事件 是某事发生的信号。所有的 DOM 节点都生成这样的信号(但事件不仅限于 DOM)。
这是最有用的 DOM 事件的列表
鼠标事件:
- click —— 当鼠标点击一个元素时(触摸屏设备会在点击时生成)。
- contextmenu —— 当鼠标右键点击一个元素时。
- mouseover / mouseout —— 当鼠标指针移入/离开一个元素时。
- mousedown / mouseup —— 当在元素上按下/释放鼠标按钮时。
- mousemove —— 当鼠标移动时。
键盘事件:
- keydown 和 keyup —— 当按下和松开一个按键时。
表单(form)元素事件:
- submit —— 当访问者提交了一个
- focus —— 当访问者聚焦于一个元素时,例如聚焦于一个 。
Document 事件:
- DOMContentLoaded —— 当 HTML 的加载和处理均完成,DOM 被完全构建完成时。
CSS 事件:
- transitionend —— 当一个 CSS 动画完成时。
事件处理程序
为了对事件作出响应,我们可以分配一个 处理程序(handler)—— 一个在事件发生时运行的函数。
处理程序是在发生用户行为(action)时运行 JavaScript 代码的一种方式。
有几种分配处理程序的方法。让我们来看看,从最简单的开始。HTML 特性
处理程序可以设置在 HTML 中名为 on的特性(attribute)中。 ```javascript
// 创建一个 JavaScript 函数
<a name="dJM1T"></a>
### DOM 属性
我们可以使用 DOM 属性(property)on<event> 来分配处理程序。
```javascript
<input id="elem" type="button" value="Click me">
<script>
elem.onclick = function() {
alert('Thank you');
};
</script>
访问元素:this
处理程序中的 this 的值是对应的元素。就是处理程序所在的那个元素。
<button onclick="alert(this.innerHTML)">Click me</button>
可能出现的错误
function sayThanks() {
alert('Thanks!');
}
elem.onclick = sayThanks; // 注意:函数应该是以 sayThanks 的形式进行赋值,而不是 sayThanks()。
button.onclick = sayThanks(); // 错误
// 如果我们添加了括号,那么 sayThanks() 就变成了一个函数调用。
// 所以,最后一行代码实际上获得的是函数执行的 结果,
// 即 undefined(因为这个函数没有返回值)。此代码不会工作。
……但在标记(markup)中,我们确实需要括号:
<input type="button" id="button" onclick="sayThanks()"> // sayThanks()需要括号
DOM 属性是大小写敏感的。
将处理程序分配给 elem.onclick,而不是 elem.ONCLICK,因为 DOM 属性是大小写敏感的。
addEventListener
上述分配处理程序的方式的根本问题是 —— 我们不能为一个事件分配多个处理程序。
假设,在我们点击了一个按钮时,我们代码中的一部分想要高亮显示这个按钮,另一部分则想要显示一条消息。
我们想为此事件分配两个处理程序。但是,新的 DOM 属性将覆盖现有的 DOM 属性
多次调用 addEventListener 允许添加多个处理程序,如下所示:
<input id="elem" type="button" value="Click me"/>
<script>
function handler1() {
alert('Thanks!');
};
function handler2() {
alert('Thanks again!');
}
elem.onclick = () => alert("Hello");
elem.addEventListener("click", handler1); // Thanks!
elem.addEventListener("click", handler2); // Thanks again!
</script>
事件对象
对象处理程序:handleEvent
正如我们所看到的,当 addEventListener 接收一个对象作为处理程序时,在事件发生时,它就会调用 obj.handleEvent(event) 来处理事件。
<button id="elem">Click me</button>
<script>
class Menu {
handleEvent(event) {
switch(event.type) {
case 'mousedown':
elem.innerHTML = "Mouse button pressed";
break;
case 'mouseup':
elem.innerHTML += "...and released.";
break;
}
}
}
let menu = new Menu();
elem.addEventListener('mousedown', menu);
elem.addEventListener('mouseup', menu);
</script>
冒泡和捕获
处理程序(handler)被分配给了
),该处理程序也会运行:
<div onclick="alert('The handler!')">
<em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>
冒泡
冒泡(bubbling)原理很简单。
当一个事件发生在一个元素上,它会首先运行在该元素上的处理程序,然后运行其父元素上的处理程序,然后一直向上到其他祖先上的处理程序。
假设我们有 3 层嵌套 FORM > DIV > P,它们各自拥有一个处理程序:
<style>
body * {
margin: 10px;
border: 1px solid blue;
}
</style>
<form onclick="alert('form')">FORM
<div onclick="alert('div')">DIV
<p onclick="alert('p')">P</p>
</div>
</form>
点击内部的
会首先运行 onclick:
- 在该
上的。
- 然后是外部 上的。
- 然后是外部 上的。
- 以此类推,直到最后的 document 对象。
如果我们点击
,那么我们将看到 3 个 alert:p → div → form。
这个过程被称为“冒泡(bubbling)”,因为事件从内部元素“冒泡”到所有父级,就像在水里的气泡一样。
几乎 所有事件都会冒泡。例如,focus 事件不会冒泡。同样,我们以后还会遇到其他例子。但这仍然是例外,而不是规则,大多数事件的确都是冒泡的。
event.target
父元素上的处理程序始终可以获取事件实际发生位置的详细信息。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。
注意与 this(=event.currentTarget)之间的区别:
- event.target —— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。
- this —— 是“当前”元素,其中有一个当前正在运行的处理程序。
停止冒泡
冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 ,然后再到 document 对象,有些事件甚至会到达 window,它们会调用路径上所有的处理程序。
但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。
用于停止冒泡的方法是 event.stopPropagation()。
例如,如果你点击
- 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
- 目标阶段(Target phase)—— 事件到达目标元素。
- 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。
冒泡和捕获为“事件委托”奠定了基础 —— 一种非常强大的事件处理模式,我们将在下一章中进行研究。
事件委托
这个想法是,如果我们有许多以类似方式处理的元素,那么就不必为每个元素分配一个处理程序 —— 而是将单个处理程序放在它们的共同祖先上。
例子:我们的任务是在点击时高亮显示被点击的单元格 。
与其为每个 (可能有很多)分配一个 onclick 处理程序 —— 我们可以在
元素上设置一个“捕获所有”的处理程序。 它将使用 event.target 来获取点击的元素并高亮显示它。let selectedTd;
table.onclick = function(event) {
let target = event.target; // 在哪里点击的?
if (target.tagName != 'TD') return; // 不在 TD 上?那么我们就不会在意
highlight(target); // 高亮显示它
};
function highlight(td) {
if (selectedTd) { // 移除现有的高亮显示,如果有的话
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // 高亮显示新的 td
}
委托示例:标记中的行为
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
“行为”模式
行为模式分为两个部分:
- 我们将自定义特性添加到描述其行为的元素。
- 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上 —— 则执行行为(action)。
行为:计数器
例如,这里的特性 data-counter 给按钮添加了一个“点击增加”的行为。
```javascript
Counter:
One more counter:
<a name="AiBQU"></a>
### 行为:切换器
点击一个具有 data-toggle-id 特性的元素将显示/隐藏具有给定 id 的元素:
```javascript
<button data-toggle-id="subscribe-mail">
Show the subscription form
</button>
<form id="subscribe-mail" hidden>
Your mail: <input type="email">
</form>
<script>
document.addEventListener('click', function(event) {
let id = event.target.dataset.toggleId;
if (!id) return;
let elem = document.getElementById(id);
elem.hidden = !elem.hidden;
});
</script>
向元素添加切换功能 —— 无需了解 JavaScript,只需要使用特性 data-toggle-id 即可。
这可能变得非常方便 —— 无需为每个这样的元素编写 JavaScript。只需要使用行为。文档级处理程序使其适用于页面的任意元素。
总结
事件委托真的很酷!这是 DOM 事件最有用的模式之一。
它通常用于为许多相似的元素添加相同的处理,但不仅限于此。
算法:
- 在容器(container)上放一个处理程序。
- 在处理程序中 —— 检查源元素 event.target。
- 如果事件发生在我们感兴趣的元素内,那么处理该事件。
事件委托也有其局限性:
- 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 event.stopPropagation()。
其次,委托可能会增加 CPU 负载,因为容器级别的处理程序会对容器中任意位置的事件做出反应,而不管我们是否对该事件感兴趣。但是,通常负载可以忽略不计,所以我们不考虑它。
浏览器默认行为
许多事件会自动触发浏览器执行某些行为。如果我们使用 JavaScript 处理一个事件,那么我们通常不希望发生相应的浏览器行为。而是想要实现其他行为进行替代。
阻止浏览器行为
有两种方式来告诉浏览器我们不希望它执行默认行为:
主流的方式是使用 event 对象。有一个 event.preventDefault() 方法。
- 如果处理程序是使用 on
(而不是 addEventListener)分配的,那返回 false 也同样有效。
处理程序选项 “passive”
addEventListener 的可选项 passive: true 向浏览器发出信号,表明处理程序将不会调用 preventDefault()。
event.defaultPrevented
创建自定义事件
我们不仅可以分配事件处理程序,还可以从 JavaScript 生成事件。
事件构造器
内建事件类形成一个层次结构(hierarchy),类似于 DOM 元素类。根是内建的 Event 类。
我们可以像这样创建 Event 对象:let event = new Event(type[, options]);
dispatchEvent
事件对象被创建后,我们应该使用 elem.dispatchEvent(event) 调用在元素上“运行”它。
在下面这个示例中,click 事件是用 JavaScript 初始化创建的。处理程序工作方式和点击按钮的方式相同:
```javascript
冒泡示例
MouseEvent,KeyboardEvent 及其他
这是一个摘自于 UI 事件规范 的一个简短的 UI 事件类列表:
- UIEvent
- FocusEvent
- MouseEvent
- WheelEvent
- KeyboardEvent
- …
event.preventDefault()
通过调用 event.preventDefault(),事件处理程序可以发出一个信号,指出这些行为应该被取消。
在这种情况下,elem.dispatchEvent(event) 的调用会返回 false。那么分派(dispatch)该事件的代码就会知道不应该再继续。
事件中的事件是同步的
通常事件是在队列中处理的。也就是说:如果浏览器正在处理 onclick,这时发生了一个新的事件,例如鼠标移动了,那么它的处理程序会被排入队列,相应的 mousemove 处理程序将在 onclick 事件处理完成后被调用。