[TOC]

事件 是某事发生的信号。所有的 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:

  1. 在该

    上的。

  2. 然后是外部
    上的。
  3. 然后是外部 上的。
  4. 以此类推,直到最后的 document 对象。

如果我们点击

,那么我们将看到 3 个 alert:p → div → form。
这个过程被称为“冒泡(bubbling)”,因为事件从内部元素“冒泡”到所有父级,就像在水里的气泡一样。
几乎 所有事件都会冒泡。例如,focus 事件不会冒泡。同样,我们以后还会遇到其他例子。但这仍然是例外,而不是规则,大多数事件的确都是冒泡的。

event.target

父元素上的处理程序始终可以获取事件实际发生位置的详细信息。
引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。
注意与 this(=event.currentTarget)之间的区别:

  • event.target —— 是引发事件的“目标”元素,它在冒泡过程中不会发生变化。
  • this —— 是“当前”元素,其中有一个当前正在运行的处理程序。

    停止冒泡

    冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 ,然后再到 document 对象,有些事件甚至会到达 window,它们会调用路径上所有的处理程序。
    但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。
    用于停止冒泡的方法是 event.stopPropagation()。
    例如,如果你点击
  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(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>

“行为”模式

行为模式分为两个部分:

  1. 我们将自定义特性添加到描述其行为的元素。
  2. 用文档范围级的处理程序追踪事件,如果事件发生在具有特定特性的元素上 —— 则执行行为(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 事件最有用的模式之一。
它通常用于为许多相似的元素添加相同的处理,但不仅限于此。
算法:

  1. 在容器(container)上放一个处理程序。
  2. 在处理程序中 —— 检查源元素 event.target。
  3. 如果事件发生在我们感兴趣的元素内,那么处理该事件。

事件委托也有其局限性:

  • 首先,事件必须冒泡。而有些事件不会冒泡。此外,低级别的处理程序不应该使用 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 事件处理完成后被调用。