理解 JavaScript:HTML 中的事件

165 阅读4分钟

事件创建

HTML 中的事件并不只是一个抽象的概念,它实实在在存在于代码之中,并且有一个名字用来标识和引用它。我们还可以通过使用 new Event() 构造函数来创建自己定义的事件。

const event = new Event('myEvent');

有时候,光是事件本身有时可能还不够用,我们还需要传递一些额外的数据或信息。为此,标准提供了一个单独的 CustomEvent 接口,它与普通事件的唯一区别就是具有一个只读的 detail 属性。

const event = new CustomEvent('pet', {
  detail: {
    type: 'dog',
    color: 'black',
  },
});

事件触发

为了让创建的事件真正发挥作用,它需要在某个对象上被触发 (dispatch)。事件可以是原生的 (由浏览器自身创建),也可以是合成的 (由开发人员通过 Web API 创建)。

下文所说的对象,都是默认指在浏览器中的 Web 元素

const myEvent = new Event('myEvent');

// 事件的调用总是与特定对象 (Web 元素) 绑定的。
document.getElementById('my-element').dispatchEvent(event);

事件监听

当在对象上触发事件时,假设有人会监听该事件。为了等待特定事件并对其做出反应,需要使用 addEventListener 方法在此对象上创建监听器函数。

window.addEventListener('myEvent', (event) => {
  // 当 "myEvent" 事件触发时该函数会被调用
});

如果不再需要侦听器,可以使用 removeEventListener 方法将其删除。

// 在这两种方法中,函数本身的引用要是相同的
// 这一点很重要,不然监听器将不会被删除
const callback = (event) => {
  /* ... */
};

window.addEventListener('myEvent', callback);
window.removeEventListener('myEvent', callback);

事件阶段

HTML 标准定义了事件的几个阶段:

  • NONE - 事件尚未调度。在这个阶段正是事件创建之后。

  • CAPTURING_PHASE - 捕获阶段。事件在触发之后、到达目标对象之前发生。

  • AT_TARGET- 目标阶段。事件在到达目标对象时立即发生。

  • BUBBLING_PHASE - 冒泡阶段。事件在到达目标对象之后发生。

NONEAT_TARGET 这两个阶段还可以理解,但是为什么还有在到达目标对象之前和之后这两个阶段。为了解答这个问题,还需要继续深入事件机制,大家接着往下看。

事件捕捉

我们来看看下面的 DOM 树结构。

<div id="block-1">
    <div id="block-2">
        <div id="block-3">
        </div>
    </div>
</div>

下面的 JavaScript 代码我们将事件监听器附加到所有对象,并在 #block-3 元素上触发一个事件。

for (let i = 1; i <= 3; i++) {
  document.getElementById(`block-${i}`).addEventListener(
    "myEvent",
    () => { console.log(`block-${i}`) }
  );
}

const event = new Event("myEvent");

document.getElementById("block-3").dispatchEvent(event);

// 输出
// block-3

枯燥的代码示例还是比较无趣,可以看看下面这个可交互的例子,模拟了上面代码示例的事件触发流程。

可以看到,在这种情况下,监听器只会在 #block-3AT_TARGET 阶段)被激活。然而,Web API 允许父容器“捕获”来自其子容器的事件。为此,需要在事件监听器上把 capture 属性设置为 true

for (let i = 1; i <= 3; i++) {
  document.getElementById(`block-${i}`).addEventListener(
    "myEvent",
    () => { console.log(`block-${i}`) },
    {
      capture: true,
    }
  );
}
// 输出
// block-1
// block-2
// block-3

现在,通过在第三个块上触发事件,事件将在到达其目标 - #block-3 元素之前按顺序穿过所有父元素。当事件从第一个块遍历到第三个块时,它将处于 CAPTURING_PHASE 中。

事件冒泡

接下来都是用同一个例子来举例。但是,这次我们不会在监听器上设置 capture 标志。相反,我们将在事件触发器上把 bubbles 设置为 true

document.getElementById("block-3").dispatchEvent(event, {
  bubbles: true,
});

现在事件在到达目标对象后,事件将“向上”传播,从第三个块到第一个块,就像“冒泡”一样。此时,事件将处于 BUBBLING_PHASE 中。

停止事件

除了在事件的不同阶段进行监听之外,Web API 还允许停止事件的进一步传播。可以使用 event.stopPropagation() 方法来停止事件的传播。

document.getElementById("block-3").addEventListener("myEvent",
  (event) => {
    event.stopPropagation();
  }, {
    capture: true,
  }
);

理解事件

事件捕获和冒泡是通过不同地方的设置来控制的。事件捕获在监听器本身上激活,而冒泡在事件调用时启用。这允许以各种方式组合这些阶段。例如,可以只在块 1 和块 3 上启用捕获,然后其余部分可以在冒泡阶段工作。

重要的是要理解一个监听器不能同时在多个阶段工作。一旦某个阶段触发了监听器,下一阶段就不会再调用该监听器。

但是,没有什么可以阻止我们将多个监听器附加到单个对象,例如:

document.getElementById("block-3").addEventListener("myEvent", callback, {
  capture: true,
});

document.getElementById("block-3").addEventListener("myEvent", callback, {
  capture: false,
});

将多个监听器附加到同一个对象上,这样就可以达到对象可以同时监听CAPTURING_PHASEBUBBLING_PHASE 这两个阶段的事件。下面的例子展示了这些阶段按严格的顺序发生:CAPTURING_PHASE -> AT_TARGET -> BUBBLING_PHASE