2022 年 10 月 14 日

冒泡和捕获

我们从一个示例开始。

此处理程序被分配给 <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>

这有点奇怪吧?如果实际点击的是 <em>,为什么 <div> 上的处理程序会运行?

冒泡

冒泡原理很简单。

当元素上发生事件时,它首先运行其上的处理程序,然后运行其父元素上的处理程序,然后一直向上运行到其他祖先元素上。

假设我们有 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> 上单击首先运行 onclick

  1. 在该 <p> 上。
  2. 然后在外部 <div> 上。
  3. 然后在外部 <form> 上。
  4. 依此类推,一直向上到 document 对象。

因此,如果我们单击 <p>,那么我们将看到 3 个警报:pdivform

该过程称为“冒泡”,因为事件像水中的气泡一样从内部元素向上“冒泡”到父元素。

几乎所有事件都会冒泡。

此短语中的关键词是“几乎”。

例如,focus 事件不会冒泡。还有其他示例,我们稍后会了解它们。但它仍然是一个例外,而不是规则,大多数事件确实会冒泡。

event.target

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

导致事件的最深层嵌套元素称为目标元素,可作为 event.target 访问。

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

  • event.target – 是引发事件的“目标”元素,它不会在冒泡过程中改变。
  • this – 是“当前”元素,即当前正在运行处理程序的元素。

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

form.onclick 处理程序中

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

查看

结果
script.js
example.css
index.html
form.onclick = function(event) {
  event.target.style.backgroundColor = 'yellow';

  // chrome needs some time to paint yellow
  setTimeout(() => {
    alert("target = " + event.target.tagName + ", this=" + this.tagName);
    event.target.style.backgroundColor = ''
  }, 0);
};
form {
  background-color: green;
  position: relative;
  width: 150px;
  height: 150px;
  text-align: center;
  cursor: pointer;
}

div {
  background-color: blue;
  position: absolute;
  top: 25px;
  left: 25px;
  width: 100px;
  height: 100px;
}

p {
  background-color: red;
  position: absolute;
  top: 25px;
  left: 25px;
  width: 50px;
  height: 50px;
  line-height: 50px;
  margin: 0;
}

body {
  line-height: 25px;
  font-size: 16px;
}
<!DOCTYPE HTML>
<html>

<head>
  <meta charset="utf-8">
  <link rel="stylesheet" href="example.css">
</head>

<body>
  A click shows both <code>event.target</code> and <code>this</code> to compare:

  <form id="form">FORM
    <div>DIV
      <p>P</p>
    </div>
  </form>

  <script src="script.js"></script>
</body>
</html>

event.target 可能等于 this – 当直接在 <form> 元素上单击时会发生这种情况。

阻止冒泡

冒泡事件从目标元素直接向上触发。通常,它会一直向上触发到 <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>
event.stopImmediatePropagation()

如果一个元素对单个事件有多个事件处理程序,那么即使其中一个停止冒泡,其他处理程序仍然会执行。

换句话说,event.stopPropagation() 停止向上移动,但在当前元素上所有其他处理程序仍会运行。

要停止冒泡并阻止当前元素上的处理程序运行,可以使用 event.stopImmediatePropagation() 方法。在此之后,不会执行任何其他处理程序。

不要无缘无故地停止冒泡!

冒泡很方便。不要在没有真正需要的情况下停止冒泡:显而易见且经过精心设计的架构。

有时 event.stopPropagation() 会创建隐藏的陷阱,这些陷阱以后可能会成为问题。

例如

  1. 我们创建一个嵌套菜单。每个子菜单处理其元素上的点击事件并调用 stopPropagation,以便外部菜单不会触发。
  2. 稍后,我们决定捕获整个窗口上的点击事件,以跟踪用户的行为(人们点击的位置)。一些分析系统会这样做。通常,代码使用 document.addEventListener('click'…) 来捕获所有点击事件。
  3. 我们的分析在 stopPropagation 停止点击的区域上不起作用。遗憾的是,我们遇到了“死区”。

通常没有真正的需要阻止冒泡。看似需要这样做的任务可以通过其他方式解决。其中之一是使用自定义事件,我们稍后会介绍。我们还可以在一个处理程序中将我们的数据写入 event 对象并在另一个处理程序中读取它,以便我们可以将有关下面处理的信息传递给父级处理程序。

捕获

还有另一个称为“捕获”的事件处理阶段。它在实际代码中很少使用,但有时可能很有用。

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

  1. 捕获阶段 - 事件向下传递到元素。
  2. 目标阶段 - 事件到达目标元素。
  3. 冒泡阶段 - 事件从元素向上冒泡。

以下是摘自规范的图片,展示了针对表格中 <td> 上的单击事件的捕获 (1)、目标 (2) 和冒泡 (3) 阶段

也就是说:对于 <td> 上的单击,事件首先沿着祖先链向下传递到元素(捕获阶段),然后到达目标并在那里触发(目标阶段),然后向上(冒泡阶段),在途中调用处理程序。

到目前为止,我们只讨论了冒泡,因为捕获阶段很少使用。

事实上,捕获阶段对我们来说是不可见的,因为使用 on<event> 属性或使用 HTML 属性或使用双参数 addEventListener(event, handler) 添加的处理程序不知道任何有关捕获的信息,它们只在第 2 和第 3 阶段运行。

要在捕获阶段捕获事件,我们需要将处理程序 capture 选项设置为 true

elem.addEventListener(..., {capture: true})

// or, just "true" is an alias to {capture: true}
elem.addEventListener(..., true)

capture 选项有两个可能的值

  • 如果它为 false(默认),则在冒泡阶段设置处理程序。
  • 如果它为 true,则在捕获阶段设置处理程序。

请注意,虽然正式有 3 个阶段,但第 2 阶段(“目标阶段”:事件到达元素)不会单独处理:捕获和冒泡阶段的处理程序都会在该阶段触发。

让我们看看捕获和冒泡的实际操作

<style>
  body * {
    margin: 10px;
    border: 1px solid blue;
  }
</style>

<form>FORM
  <div>DIV
    <p>P</p>
  </div>
</form>

<script>
  for(let elem of document.querySelectorAll('*')) {
    elem.addEventListener("click", e => alert(`Capturing: ${elem.tagName}`), true);
    elem.addEventListener("click", e => alert(`Bubbling: ${elem.tagName}`));
  }
</script>

代码在文档中的每个元素上设置点击处理程序,以查看哪些元素正在工作。

如果您单击 <p>,则顺序为

  1. HTMLBODYFORMDIV -> P(捕获阶段,第一个侦听器)
  2. PDIVFORMBODYHTML(冒泡阶段,第二个侦听器)。

请注意,P 出现了两次,因为我们设置了两个侦听器:捕获和冒泡。目标在第一阶段的末尾和第二阶段的开始触发。

有一个属性 event.eventPhase,它告诉我们事件被捕获的阶段数。但它很少使用,因为我们通常在处理程序中知道它。

要删除处理程序,removeEventListener 需要相同的阶段

如果我们 addEventListener(..., true),那么我们应该在 removeEventListener(..., true) 中提到相同的阶段,以正确删除处理程序。

同一元素和同一阶段上的侦听器按其设置顺序运行

如果我们在同一阶段对同一元素有多个事件处理程序,并使用 addEventListener 分配给该元素,则它们将按创建顺序运行

elem.addEventListener("click", e => alert(1)); // guaranteed to trigger first
elem.addEventListener("click", e => alert(2));
捕获期间的 event.stopPropagation() 也阻止了冒泡

event.stopPropagation() 方法及其兄弟方法 event.stopImmediatePropagation() 也可以在捕获阶段调用。这样,不仅会停止进一步的捕获,还会停止冒泡。

换句话说,通常事件会先向下(“捕获”)然后向上(“冒泡”)。但是,如果在捕获阶段调用 event.stopPropagation(),则事件传播将停止,不会发生冒泡。

摘要

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

  • 然后,事件从文档根向下移动到 event.target,在途中调用使用 addEventListener(..., true) 分配的处理程序(true{capture: true} 的简写)。
  • 然后,在目标元素本身上调用处理程序。
  • 然后,事件从 event.target 向上冒泡到根,调用使用 on<event>、HTML 属性和 addEventListener(不带第三个参数或第三个参数为 false/{capture:false})分配的处理程序。

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

  • event.target——引发事件的最深层元素。
  • event.currentTarget (=this)——处理事件的当前元素(在其上有处理程序的元素)
  • event.eventPhase——当前阶段(捕获=1,目标=2,冒泡=3)。

任何事件处理程序都可以通过调用 event.stopPropagation() 停止事件,但这是不推荐的,因为我们无法真正确定在上面是否需要它,也许是用于完全不同的东西。

捕获阶段很少使用,我们通常在冒泡时处理事件。对此有一个合乎逻辑的解释。

在现实世界中,当事故发生时,地方当局会首先做出反应。他们最了解事故发生的地点。然后,如果需要,上级当局会做出反应。

事件处理程序也是如此。在特定元素上设置处理程序的代码了解有关该元素及其执行操作的最大详细信息。特定 <td> 上的处理程序可能完全适合该 <td>,它了解有关该 <td> 的所有信息,因此它应该首先获得机会。然后,它的直接父级也了解上下文,但了解得少一些,依此类推,直到处理一般概念并运行最后一个概念的最顶层元素。

冒泡和捕获为“事件委托”奠定了基础——这是一种极其强大的事件处理模式,我们将在下一章中学习。

教程地图

评论

在评论前请阅读此内容…
  • 如果您有改进建议 - 请 提交 GitHub 问题 或提交拉取请求,而不是发表评论。
  • 如果您无法理解文章中的某些内容 – 请详细说明。
  • 要插入几行代码,请使用 <code> 标记,对于多行代码 – 将它们包装在 <pre> 标记中,对于 10 行以上的代码 – 使用沙盒 (plnkrjsbincodepen…)