最近開了一個讀者回饋表單郵箱,無論是對文章的感想或是對部落格的感想,有什麼想回饋的都可以發郵箱跟我說:i_kkkp@163.com

浏览器事件的冒泡和捕获

前言

让我们从一个示例开始。

处理程序(handler)被分配给了 <div>,但是如果你点击任何嵌套的标签(例如 <em><code>),该处理程序也会运行:

<div onclick="alert('The handler!')">
  <em>If you click on <code>EM</code>, the handler on <code>DIV</code> runs.</em>
</div>

So, if you click on EM, the handler on DIV runs.

冒泡

冒泡(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>

现在,如果你点击 <p>,那么会发生什么?

  1. 首先,处理程序在 <p> 上运行。

  2. 然后,它在 <div> 上运行。

  3. 然后,它在 <form> 上运行。

  4. 最后,它在 document 上运行。

这种行为被称为“事件冒泡”,因为它像气泡一样从元素冒出来。

因此,如果我们点击 <p>,那么我们将看到 3 个 alert:p → div → form。

event.target

父元素上的处理程序始终可以获取事件实际发生位置的详细信息。

引发事件的那个嵌套层级最深的元素被称为目标元素,可以通过 event.target 访问。

注意与 this(=event.currentTarget)之间的区别:

  • this 是在处理程序运行时的“当前”元素,它始终相同。

  • event.target 是在事件发生时的“目标”元素,它可以是任何元素,它在冒泡过程中改变。

那我们来举个例子吧!

例如,如果我们有一个处理程序 form.onclick,那么它可以“捕获”表单内的所有点击。无论点击发生在哪里,它都会冒泡到 <form> 并运行处理程序。

注意哈!这边是将所有的事件都绑定到了 form 上,而不是每个元素上。

在 form.onclick 处理程序中:

this(=event.currentTarget)是 <form> 元素,因为处理程序在它上面运行。
event.target 是表单中实际被点击的元素。

A click shows both event.target and this to compare:

FORM
DIV

P

停止冒泡

冒泡事件从目标元素开始向上冒泡。通常,它会一直上升到 <html>,然后再到 document 对象,有些事件甚至会到达 window,它们会调用路径上所有的处理程序。

但是任意处理程序都可以决定事件已经被完全处理,并停止冒泡。

用于停止冒泡的方法是 event.stopPropagation()。

例如,如果你点击 <button>,这里的 body.onclick 不会工作:

<body onclick="alert(`the bubbling doesn't reach here`)">
  <button onclick="event.stopPropagation()">Click me</button>
</body>

捕获

事件处理的另一个阶段被称为“捕获(capturing)”。

DOM 事件标准描述了事件传播的 3 个阶段:

  1. 捕获阶段(Capturing phase)—— 事件(从 Window)向下走近元素。
  2. 目标阶段(Target phase)—— 事件到达目标元素。
  3. 冒泡阶段(Bubbling phase)—— 事件从元素上开始冒泡。

下面是在表格中点击 <td> 的图片,摘自规范:

Event-Bubbling

也就是说:点击 <td>,事件首先通过祖先链向下到达元素(捕获阶段),然后到达目标(目标阶段),最后上升(冒泡阶段),在途中调用处理程序。

之前,我们只讨论了冒泡,因为捕获阶段很少被使用。通常我们看不到它。

使用 on<event> 属性或使用 HTML 特性(attribute)或使用两个参数的 addEventListener(event, handler) 添加的处理程序,对捕获一无所知,它们仅在第二阶段和第三阶段运行。

总结

当一个事件发生时 —— 发生该事件的嵌套最深的元素被标记为“目标元素”(event.target)。

  • 然后,事件从文档根节点向下移动到 event.target,并在途中调用分配了 addEventListener(…, true) 的处理程序(true 是 {capture: true} 的一个简写形式)。
  • 然后,在目标元素自身上调用处理程序。
  • 然后,事件从 event.target 冒泡到根,调用使用 on<event>、HTML 特性(attribute)和没有第三个参数的,或者第三个参数为 false/{capture:false} 的addEventListener 分配的处理程序。

每个处理程序都可以访问 event 对象的属性:

  • event.target —— 引发事件的层级最深的元素。
  • event.currentTarget(=this)—— 处理事件的当前元素(具有处理程序的元素)
  • event.eventPhase —— 当前阶段(capturing=1,target=2,bubbling=3)。

任何事件处理程序都可以通过调用 event.stopPropagation() 来停止事件,但不建议这样做,因为我们不确定是否确实不需要冒泡上来的事件,也许是用于完全不同的事情。

捕获阶段很少使用,通常我们会在冒泡时处理事件。这背后有一个逻辑。

事件处理程序也是如此。在特定元素上设置处理程序的代码,了解有关该元素最详尽的信息。特定于 的处理程序可能恰好适合于该 ,这个处理程序知道关于该元素的所有信息。所以该处理程序应该首先获得机会。然后,它的直接父元素也了解相关上下文,但了解的内容会少一些,以此类推,直到处理一般性概念并运行最后一个处理程序的最顶部的元素为止。

浏览器事件委托 Vue Components 和 Web 组件Custom Elements

評論