事件冒泡

我们已经看到,网页由元素组成 - 标题、文本段落、图片、按钮等 - 你可以监听发生在这些元素上的事件。例如,你可以向按钮添加一个监听器,当用户单击按钮时,它将运行。

¥We've seen that a web page is composed of elements — headings, paragraphs of text, images, buttons, and so on — and that you can listen for events that happen to these elements. For example, you could add a listener to a button, and it will run when the user clicks the button.

我们还看到这些元素可以相互嵌套:例如,<button> 可以放在 <div> 元素内。在这种情况下,我们将 <div> 元素称为父元素,将 <button> 称为子元素。

¥We've also seen that these elements can be nested inside each other: for example, a <button> could be placed inside a <div> element. In this case we'd call the <div> element a parent element, and the <button> a child element.

在本章中,我们将看到当你向父元素添加事件监听器并且用户单击子元素时会发生什么。

¥In this chapter we'll see what happens when you add an event listener to a parent element, and the user clicks the child element.

引入事件冒泡

¥Introducing event bubbling

在父元素上设置监听器

¥Setting a listener on a parent element

考虑这样一个网页:

¥Consider a web page like this:

html
<div id="container">
  <button>Click me!</button>
</div>
<pre id="output"></pre>

这里按钮位于另一个元素(<div> 元素)内。我们说这里的 <div> 元素是它所包含的元素的父元素。如果我们向父级添加单击事件处理程序,然后单击按钮,会发生什么?

¥Here the button is inside another element, a <div> element. We say that the <div> element here is the parent of the element it contains. What happens if we add a click event handler to the parent, then click the button?

js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
container.addEventListener("click", handleClick);

你将看到当用户单击按钮时父级会触发单击事件:

¥You'll see that the parent fires a click event when the user clicks the button:

You clicked on a DIV element

这是有道理的:该按钮位于 <div> 内部,因此当你单击该按钮时,你也隐式单击了其内部的元素。

¥This makes sense: the button is inside the <div>, so when you click the button you're also implicitly clicking the element it is inside.

冒泡示例

¥Bubbling example

如果我们向按钮和父级添加事件监听器会发生什么?

¥What happens if we add event listeners to the button and the parent?

html
<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>

让我们尝试向按钮、其父级 (<div>) 以及包含它们的 <body> 元素添加单击事件处理程序:

¥Let's try adding click event handlers to the button, its parent (the <div>), and the <body> element that contains both of them:

js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);

你将看到当用户单击按钮时,所有三个元素都会触发单击事件:

¥You'll see that all three elements fire a click event when the user clicks the button:

You clicked on a BUTTON element
You clicked on a DIV element
You clicked on a BODY element

在这种情况下:

¥In this case:

  • 首先单击按钮
  • 然后单击其父元素(<div> 元素)
  • 接下来是 <div> 元素的父元素(<body> 元素)。

我们通过说事件从被单击的最里面的元素冒泡来描述这一点。

¥We describe this by saying that the event bubbles up from the innermost element that was clicked.

此行为可能很有用,但也可能导致意外问题。在接下来的部分中,我们将看到它引起的问题,并找到解决方案。

¥This behavior can be useful and can also cause unexpected problems. In the next sections, we'll see a problem that it causes, and find the solution.

视频播放器示例

¥Video player example

在此示例中,我们的页面包含一个最初隐藏的视频和一个标记为 "显示视频" 的按钮。我们想要以下交互:

¥In this example our page contains a video, which is hidden initially, and a button labeled "Display video". We want the following interaction:

  • 当用户单击 "显示视频" 按钮时,显示包含视频的框,但尚未开始播放视频。
  • 当用户单击视频时,开始播放视频。
  • 当用户单击视频外部框中的任意位置时,隐藏该框。

HTML 看起来像这样:

¥The HTML looks like this:

html
<button>Display video</button>

<div class="hidden">
  <video>
    <source
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
      type="video/webm" />
    <p>
      Your browser doesn't support HTML video. Here is a
      <a href="rabbit320.mp4">link to the video</a> instead.
    </p>
  </video>
</div>

这包括:

¥It includes:

  • <button> 元素
  • 最初具有 class="hidden" 属性的 <div> 元素
  • 嵌套在 <div> 元素内的 <video> 元素。

我们使用 CSS 来隐藏具有 "hidden" 类集的元素。

¥We're using CSS to hide elements with the "hidden" class set.

css
div {
  width: 100%;
  height: 100%;
  background-color: #eee;
}

.hidden {
  display: none;
}

div video {
  padding: 40px;
  display: block;
  width: 400px;
  margin: 40px auto;
}

JavaScript 看起来像这样:

¥The JavaScript looks like this:

js
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));
video.addEventListener("click", () => video.play());
box.addEventListener("click", () => box.classList.add("hidden"));

这添加了三个 'click' 事件监听器:

¥This adds three 'click' event listeners:

  • <button> 上的一个,显示包含 <video><div>
  • <video> 上的一个,开始播放视频
  • <div> 上的一个,隐藏视频

让我们看看这是如何工作的:

¥Let's see how this works:

你应该看到,当你单击该按钮时,会显示该框及其包含的视频。但是当你单击视频时,视频开始播放,但该框再次隐藏!

¥You should see that when you click the button, the box and the video it contains are shown. But then when you click the video, the video starts to play, but the box is hidden again!

该视频位于 <div> 内部(它是其中的一部分),因此单击视频会运行两个事件处理程序,从而导致此行为。

¥The video is inside the <div> — it is part of it — so clicking the video runs both the event handlers, causing this behavior.

修复 stopPropagation() 的问题

¥Fixing the problem with stopPropagation()

正如我们在上一节中看到的,事件冒泡有时会产生问题,但有一种方法可以防止它。Event 对象有一个名为 stopPropagation() 的可用函数,当在事件处理程序内调用该函数时,可以防止事件冒泡到任何其他元素。

¥As we saw in the last section, event bubbling can sometimes create problems, but there is a way to prevent it. The Event object has a function available on it called stopPropagation() which, when called inside an event handler, prevents the event from bubbling up to any other elements.

我们可以通过将 JavaScript 更改为以下内容来解决当前的问题:

¥We can fix our current problem by changing the JavaScript to this:

js
const btn = document.querySelector("button");
const box = document.querySelector("div");
const video = document.querySelector("video");

btn.addEventListener("click", () => box.classList.remove("hidden"));

video.addEventListener("click", (event) => {
  event.stopPropagation();
  video.play();
});

box.addEventListener("click", () => box.classList.add("hidden"));

我们在这里所做的就是在 <video> 元素的 'click' 事件的处理程序中对事件对象调用 stopPropagation()。这将阻止该事件冒泡到框中。现在尝试单击按钮,然后单击视频:

¥All we're doing here is calling stopPropagation() on the event object in the handler for the <video> element's 'click' event. This will stop that event from bubbling up to the box. Now try clicking the button and then the video:

html
<button>Display video</button>

<div class="hidden">
  <video>
    <source
      src="https://interactive-examples.mdn.mozilla.net/media/cc0-videos/flower.webm"
      type="video/webm" />
    <p>
      Your browser doesn't support HTML video. Here is a
      <a href="rabbit320.mp4">link to the video</a> instead.
    </p>
  </video>
</div>
css
div {
  width: 100%;
  height: 100%;
  background-color: #eee;
}

.hidden {
  display: none;
}

div video {
  padding: 40px;
  display: block;
  width: 400px;
  margin: 40px auto;
}

事件捕捉

¥Event capture

事件传播的另一种形式是事件捕获。这就像事件冒泡,但顺序相反:因此,事件不是首先在最里面的目标元素上触发,然后在连续较少的嵌套元素上触发,而是首先在最少的嵌套元素上触发,然后在连续的更多嵌套元素上触发,直到达到目标。

¥An alternative form of event propagation is event capture. This is like event bubbling but the order is reversed: so instead of the event firing first on the innermost element targeted, and then on successively less nested elements, the event fires first on the least nested element, and then on successively more nested elements, until the target is reached.

默认情况下禁用事件捕获。要启用它,你必须在 addEventListener() 中传递 capture 选项。

¥Event capture is disabled by default. To enable it you have to pass the capture option in addEventListener().

这个例子就像我们之前看到的 冒泡的例子 一样,只是我们使用了 capture 选项:

¥This example is just like the bubbling example we saw earlier, except that we have used the capture option:

html
<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>
js
const output = document.querySelector("#output");
function handleClick(e) {
  output.textContent += `You clicked on a ${e.currentTarget.tagName} element\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick, { capture: true });
container.addEventListener("click", handleClick, { capture: true });
button.addEventListener("click", handleClick);

在这种情况下,消息的顺序相反:首先触发 <body> 事件处理程序,然后是 <div> 事件处理程序,最后是 <button> 事件处理程序:

¥In this case, the order of messages is reversed: the <body> event handler fires first, followed by the <div> event handler, followed by the <button> event handler:

You clicked on a BODY element
You clicked on a DIV element
You clicked on a BUTTON element

为什么要费心捕获和冒泡呢?在过去的糟糕日子里,浏览器的交叉兼容性远不如现在,Netscape 只使用事件捕获,而 Internet Explorer 只使用事件冒泡。当 W3C 决定尝试标准化行为并达成共识时,他们最终得到了这个包含两者的系统,这就是现代浏览器所实现的。

¥Why bother with both capturing and bubbling? In the bad old days, when browsers were much less cross-compatible than now, Netscape only used event capturing, and Internet Explorer used only event bubbling. When the W3C decided to try to standardize the behavior and reach a consensus, they ended up with this system that included both, which is what modern browsers implement.

默认情况下,几乎所有事件处理程序都在冒泡阶段注册,这在大多数情况下更有意义。

¥By default almost all event handlers are registered in the bubbling phase, and this makes more sense most of the time.

事件委托

¥Event delegation

在上一节中,我们研究了事件冒泡引起的问题以及如何修复它。不过,事件冒泡不仅令人烦恼:它可能非常有用。特别是,它支持事件委托。在这种实践中,当我们希望在用户与大量子元素中的任何一个交互时运行某些代码时,我们在其父元素上设置事件监听器,并将发生在它们上的事件向上冒泡到其父元素,而不必 分别为每个子级设置事件监听器。

¥In the last section, we looked at a problem caused by event bubbling and how to fix it. Event bubbling isn't just annoying, though: it can be very useful. In particular, it enables event delegation. In this practice, when we want some code to run when the user interacts with any one of a large number of child elements, we set the event listener on their parent and have events that happen on them bubble up to their parent rather than having to set the event listener on every child individually.

让我们回到第一个示例,当用户单击按钮时,我们设置整个页面的背景颜色。假设页面分为 16 个图块,并且我们希望在用户单击该图块时将每个图块设置为随机颜色。

¥Let's go back to our first example, where we set the background color of the whole page when the user clicked a button. Suppose that instead, the page is divided into 16 tiles, and we want to set each tile to a random color when the user clicks that tile.

这是 HTML:

¥Here's the HTML:

html
<div id="container">
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
  <div class="tile"></div>
</div>

我们有一些 CSS 来设置图块的大小和位置:

¥We have a little CSS, to set the size and position of the tiles:

css
.tile {
  height: 100px;
  width: 25%;
  float: left;
}

现在,在 JavaScript 中,我们可以为每个图块添加一个单击事件处理程序。但一个更简单、更有效的选择是在父级上设置单击事件处理程序,并依靠事件冒泡来确保当用户单击图块时执行处理程序:

¥Now in JavaScript, we could add a click event handler for every tile. But a much simpler and more efficient option is to set the click event handler on the parent, and rely on event bubbling to ensure that the handler is executed when the user clicks on a tile:

js
function random(number) {
  return Math.floor(Math.random() * number);
}

function bgChange() {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  return rndCol;
}

const container = document.querySelector("#container");

container.addEventListener("click", (event) => {
  event.target.style.backgroundColor = bgChange();
});

输出如下(尝试单击它):

¥The output is as follows (try clicking around on it):

注意:在此示例中,我们使用 event.target 来获取事件目标的元素(即最里面的元素)。如果我们想访问处理此事件的元素(在本例中为容器),我们可以使用 event.currentTarget

¥Note: In this example, we're using event.target to get the element that was the target of the event (that is, the innermost element). If we wanted to access the element that handled this event (in this case the container) we could use event.currentTarget.

注意:完整源代码请参见 useful-eventtarget.html;也可以在这里看到它 实时运行

¥Note: See useful-eventtarget.html for the full source code; also see it running live here.

targetcurrentTarget

¥target and currentTarget

如果你仔细查看我们在本页中介绍的示例,你会发现我们正在使用事件对象的两个不同属性来访问被单击的元素。在 在父元素上设置监听器 中我们使用 event.currentTarget。但是,在 事件委托 中,我们使用的是 event.target

¥If you look closely at the examples we've introduced in this page, you'll see that we're using two different properties of the event object to access the element that was clicked. In Setting a listener on a parent element we're using event.currentTarget. However, in Event delegation, we're using event.target.

不同之处在于 target 指的是最初触发事件的元素,而 currentTarget 指的是已附加此事件处理程序的元素。

¥The difference is that target refers to the element on which the event was initially fired, while currentTarget refers to the element to which this event handler has been attached.

虽然 target 在事件冒泡时保持不变,但对于附加到层次结构中不同元素的事件处理程序,currentTarget 会有所不同。

¥While target remains the same while an event bubbles up, currentTarget will be different for event handlers that are attached to different elements in the hierarchy.

如果我们稍微调整上面的 冒泡示例,我们可以看到这一点。我们使用与之前相同的 HTML:

¥We can see this if we slightly adapt the Bubbling example above. We're using the same HTML as before:

html
<body>
  <div id="container">
    <button>Click me!</button>
  </div>
  <pre id="output"></pre>
</body>

JavaScript 几乎相同,只是我们同时记录 targetcurrentTarget

¥The JavaScript is almost the same, except we're logging both target and currentTarget:

js
const output = document.querySelector("#output");
function handleClick(e) {
  const logTarget = `Target: ${e.target.tagName}`;
  const logCurrentTarget = `Current target: ${e.currentTarget.tagName}`;
  output.textContent += `${logTarget}, ${logCurrentTarget}\n`;
}

const container = document.querySelector("#container");
const button = document.querySelector("button");

document.body.addEventListener("click", handleClick);
container.addEventListener("click", handleClick);
button.addEventListener("click", handleClick);

请注意,当我们单击按钮时,target 每次都是按钮元素,无论事件处理程序是附加到按钮本身、<div> 还是 <body>。但是 currentTarget 标识了我们当前正在运行其事件处理程序的元素:

¥Note that when we click the button, target is the button element every time, whether the event handler is attached to the button itself, to the <div>, or to the <body>. However currentTarget identifies the element whose event handler we are currently running:

target 属性通常用于事件委托,如上面的 事件委托 示例。

¥The target property is commonly used in event delegation, as in our Event delegation example above.

测试你的技能!

¥Test your skills!

你已读完本文,但你还记得最重要的信息吗?要在继续之前验证你是否已保留此信息 - 请参阅 测试你的技能:事件

¥You've reached the end of this article, but can you remember the most important information? To verify you've retained this information before you move on — see Test your skills: Events.

结论

¥Conclusion

你现在应该了解早期阶段需要了解的有关网络事件的所有信息。如前所述,事件实际上并不是核心 JavaScript 的一部分 - 它们是在浏览器 Web API 中定义的。

¥You should now know all you need to know about web events at this early stage. As mentioned, events are not really part of the core JavaScript — they are defined in browser Web APIs.

此外,重要的是要了解使用 JavaScript 的不同上下文具有不同的事件模型 - 从 Web API 到浏览器 WebExtensions 和 Node.js(服务器端 JavaScript)等其他字段。我们并不期望你现在了解所有这些字段,但是当你继续学习 Web 开发时,了解事件的基础知识肯定会有所帮助。

¥Also, it is important to understand that the different contexts in which JavaScript is used have different event models — from Web APIs to other areas such as browser WebExtensions and Node.js (server-side JavaScript). We are not expecting you to understand all of these areas now, but it certainly helps to understand the basics of events as you forge ahead with learning web development.

注意:如果你遇到困难,可以通过我们的 沟通渠道 之一与我们联系。

¥Note: If you get stuck, you can reach out to us in one of our communication channels.

也可以看看

¥See also

  • domevents.dev - 一个非常有用的交互式游乐场应用,可以通过探索来了解 DOM 事件系统的行为。
  • 事件参考
  • 事件顺序(关于捕获和冒泡的讨论) - Peter-Paul Koch 的一篇非常详细的文章。