事件简介

事件是你正在编程的系统中发生的事情,系统会告诉你这些信息,以便你的代码可以对它们做出反应。

¥Events are things that happen in the system you are programming, which the system tells you about so your code can react to them.

例如,如果用户单击网页上的按钮,你可能希望通过显示信息框来对该操作做出反应。在本文中,我们讨论有关事件的一些重要概念,并了解它们在浏览器中的工作原理。这不会是一项详尽的研究;这正是你现阶段需要了解的。

¥For example, if the user clicks a button on a webpage, you might want to react to that action by displaying an information box. In this article, we discuss some important concepts surrounding events, and look at how they work in browsers. This won't be an exhaustive study; just what you need to know at this stage.

先决条件: 对 HTML、CSS 和 JavaScript 第一步 有基本了解。
目标: 了解事件的基本理论、事件在浏览器中的工作方式以及事件在不同的编程环境中有何不同。

什么是事件?

¥What is an event?

事件是在你正在编程的系统中发生的事情 - 当事件发生时系统会产生(或 "fires")某种信号,并提供一种机制,通过该机制可以在以下情况下自动采取操作(即运行某些代码) 事件发生。事件在浏览器窗口内触发,并且往往附加到驻留在其中的特定项目。这可能是单个元素、一组元素、当前选项卡中加载的 HTML 文档或整个浏览器窗口。可能发生许多不同类型的事件。

¥Events are things that happen in the system you are programming — the system produces (or "fires") a signal of some kind when an event occurs, and provides a mechanism by which an action can be automatically taken (that is, some code running) when the event occurs. Events are fired inside the browser window, and tend to be attached to a specific item that resides in it. This might be a single element, a set of elements, the HTML document loaded in the current tab, or the entire browser window. There are many different types of events that can occur.

例如:

¥For example:

  • 用户选择、单击或将光标悬停在某个元素上。
  • 用户选择键盘上的一个键。
  • 用户调整浏览器窗口大小或关闭浏览器窗口。
  • 网页加载完毕。
  • 提交表格。
  • 视频播放、暂停或结束。
  • 发生错误。

你可以从此(以及浏览 MDN 事件参考)了解到有很多可以触发的事件。

¥You can gather from this (and from glancing at the MDN event reference) that there are a lot of events that can be fired.

要对事件做出反应,你可以将事件处理程序附加到该事件。这是在事件触发时运行的一段代码(通常是你作为程序员创建的 JavaScript 函数)。当这样的代码块被定义为响应事件而运行时,我们说我们正在注册一个事件处理程序。注意:事件处理程序有时称为事件监听器 - 对于我们的目的来说,它们几乎可以互换,尽管严格来说,它们是一起工作的。监听器监听事件发生,处理程序是响应事件发生而运行的代码。

¥To react to an event, you attach an event handler to it. This is a block of code (usually a JavaScript function that you as a programmer create) that runs when the event fires. When such a block of code is defined to run in response to an event, we say we are registering an event handler. Note: Event handlers are sometimes called event listeners — they are pretty much interchangeable for our purposes, although strictly speaking, they work together. The listener listens out for the event happening, and the handler is the code that is run in response to it happening.

注意:Web 事件不是核心 JavaScript 语言的一部分 - 它们被定义为浏览器内置 API 的一部分。

¥Note: Web events are not part of the core JavaScript language — they are defined as part of the APIs built into the browser.

一个例子:处理点击事件

¥An example: handling a click event

在以下示例中,页面中有一个 <button>

¥In the following example, we have a single <button> in the page:

html
<button>Change color</button>
css
button {
  margin: 10px;
}

然后我们有一些 JavaScript。我们将在下一节中更详细地讨论这一点,但现在我们只能说:它向按钮的 "click" 事件添加一个事件处理程序,该处理程序通过将页面背景设置为随机颜色来对该事件做出反应:

¥Then we have some JavaScript. We'll look at this in more detail in the next section, but for now we can just say: it adds an event handler to the button's "click" event, and the handler reacts to the event by setting the page background to a random color:

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

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

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});

示例输出如下。尝试点击按钮:

¥The example output is as follows. Try clicking the button:

使用 addEventListener()

¥Using addEventListener()

正如我们在上一个示例中看到的,可以触发事件的对象有一个 addEventListener() 方法,这是添加事件处理程序的推荐机制。

¥As we saw in the last example, objects that can fire events have an addEventListener() method, and this is the recommended mechanism for adding event handlers.

让我们仔细看看上一个示例中的代码:

¥Let's take a closer look at the code from the last example:

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

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

btn.addEventListener("click", () => {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
});

当用户单击按钮时,HTML <button> 元素将触发一个事件。所以它定义了一个 addEventListener() 函数,我们在这里调用它。我们传入两个参数:

¥The HTML <button> element will fire an event when the user clicks the button. So it defines an addEventListener() function, which we are calling here. We're passing in two parameters:

  • 字符串 "click",表示我们要监听点击事件。按钮可以触发许多其他事件,例如当用户将鼠标移到按钮上时触发 "mouseover",或者当用户按下某个键并且按钮获得焦点时触发 "keydown"
  • 事件发生时调用的函数。在我们的例子中,该函数生成随机 RGB 颜色并将页面 <body>background-color 设置为该颜色。

可以将处理函数设置为单独的命名函数,如下所示:

¥It is fine to make the handler function a separate named function, like this:

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

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

function changeBackground() {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.addEventListener("click", changeBackground);

监听其他事件

¥Listening for other events

按钮元素可以触发许多不同的事件。我们来实验一下。

¥There are many different events that can be fired by a button element. Let's experiment.

首先,制作 random-color-addeventlistener.html 的本地副本,并在浏览器中打开它。它只是我们已经玩过的简单随机颜色示例的副本。现在尝试依次将 click 更改为以下不同值,并观察示例中的结果:

¥First, make a local copy of random-color-addeventlistener.html, and open it in your browser. It's just a copy of the simple random color example we've played with already. Now try changing click to the following different values in turn, and observing the results in the example:

  • focusblur — 按钮聚焦和未聚焦时颜色发生变化;尝试按选项卡将焦点集中在按钮上,然后再次按选项卡将焦点从按钮上移开。这些通常用于在获得焦点时显示有关填写表单字段的信息,或者在表单字段填充了不正确的值时显示错误消息。
  • dblclick — 仅当双击按钮时颜色才会改变。
  • mouseovermouseout — 当鼠标指针悬停在按钮上或指针移离按钮时,颜色分别发生变化。

有些事件(例如 click)几乎可用于任何元素。其他的则更具体,仅在某些情况下有用:例如,play 事件仅适用于某些元素,例如 <video>

¥Some events, such as click, are available on nearly any element. Others are more specific and only useful in certain situations: for example, the play event is only available on some elements, such as <video>.

删除监听器

¥Removing listeners

如果你使用 addEventListener() 添加了事件处理程序,则可以使用 removeEventListener() 方法再次将其删除。例如,这将删除 changeBackground() 事件处理程序:

¥If you've added an event handler using addEventListener(), you can remove it again using the removeEventListener() method. For example, this would remove the changeBackground() event handler:

js
btn.removeEventListener("click", changeBackground);

还可以通过将 AbortSignal 传递到 addEventListener(),然后在拥有 AbortSignal 的控制器上调用 abort() 来删除事件处理程序。例如,要添加一个事件处理程序,我们可以使用 AbortSignal 删除该事件处理程序:

¥Event handlers can also be removed by passing an AbortSignal to addEventListener() and then later calling abort() on the controller owning the AbortSignal. For example, to add an event handler that we can remove with an AbortSignal:

js
const controller = new AbortController();

btn.addEventListener("click",
  () => {
    const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
    document.body.style.backgroundColor = rndCol;
  },
  { signal: controller.signal } // pass an AbortSignal to this handler
);

然后可以像这样删除上面代码创建的事件处理程序:

¥Then the event handler created by the code above can be removed like this:

js
controller.abort(); // removes any/all event handlers associated with this controller

对于简单的小型程序,清理旧的、未使用的事件处理程序是没有必要的,但对于更大、更复杂的程序,它可以提高效率。此外,删除事件处理程序的功能允许你使用同一按钮在不同情况下执行不同的操作:你所要做的就是添加或删除处理程序。

¥For simple, small programs, cleaning up old, unused event handlers isn't necessary, but for larger, more complex programs, it can improve efficiency. Also, the ability to remove event handlers allows you to have the same button performing different actions in different circumstances: all you have to do is add or remove handlers.

为单个事件添加多个监听器

¥Adding multiple listeners for a single event

通过对 addEventListener() 进行多次调用并提供不同的处理程序,你可以为单个事件拥有多个处理程序:

¥By making more than one call to addEventListener(), providing different handlers, you can have multiple handlers for a single event:

js
myElement.addEventListener("click", functionA);
myElement.addEventListener("click", functionB);

现在,当单击该元素时,这两个函数都会运行。

¥Both functions would now run when the element is clicked.

了解更多

¥Learn more

addEventListener() 还提供其他强大的功能和选项。

¥There are other powerful features and options available with addEventListener().

这些内容有点超出了本文的范围,但如果你想阅读它们,请访问 addEventListener()removeEventListener() 参考页。

¥These are a little out of scope for this article, but if you want to read them, visit the addEventListener() and removeEventListener() reference pages.

其他事件监听机制

¥Other event listener mechanisms

我们建议你使用 addEventListener() 来注册事件处理程序。它是最强大的方法,并且可以最好地适应更复杂的程序。但是,你可能会看到其他两种注册事件处理程序的方法:事件处理程序属性和内联事件处理程序。

¥We recommend that you use addEventListener() to register event handlers. It's the most powerful method and scales best with more complex programs. However, there are two other ways of registering event handlers that you might see: event handler properties and inline event handlers.

事件处理程序属性

¥Event handler properties

可以触发事件的对象(例如按钮)通常也具有名称为 on 后跟事件名称的属性。例如,元素具有属性 onclick。这称为事件处理程序属性。要监听该事件,你可以将处理程序函数分配给该属性。

¥Objects (such as buttons) that can fire events also usually have properties whose name is on followed by the name of the event. For example, elements have a property onclick. This is called an event handler property. To listen for the event, you can assign the handler function to the property.

例如,我们可以像这样重写随机颜色示例:

¥For example, we could rewrite the random-color example like this:

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

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

btn.onclick = () => {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
};

你还可以将处理程序属性设置为命名函数:

¥You can also set the handler property to a named function:

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

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

function bgChange() {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

btn.onclick = bgChange;

使用事件处理程序属性,你无法为单个事件添加多个处理程序。例如,你可以在一个元素上多次调用 addEventListener('click', handler),并在第二个参数中指定不同的函数:

¥With event handler properties, you can't add more than one handler for a single event. For example, you can call addEventListener('click', handler) on an element multiple times, with different functions specified in the second argument:

js
element.addEventListener("click", function1);
element.addEventListener("click", function2);

对于事件处理程序属性来说这是不可能的,因为任何后续尝试设置该属性都会覆盖之前的尝试:

¥This is impossible with event handler properties because any subsequent attempts to set the property will overwrite earlier ones:

js
element.onclick = function1;
element.onclick = function2;

内联事件处理程序 - 不要使用这些

¥Inline event handlers — don't use these

你可能还会在代码中看到这样的模式:

¥You might also see a pattern like this in your code:

html
<button onclick="bgChange()">Press me</button>
js
function bgChange() {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  document.body.style.backgroundColor = rndCol;
}

在 Web 上发现的最早的注册事件处理程序的方法涉及 事件处理程序 HTML 属性(或内联事件处理程序),如上所示 - 属性值实际上是事件发生时要运行的 JavaScript 代码。上面的示例调用同一页面上 <script> 元素内定义的函数,但你也可以直接在属性内插入 JavaScript,例如:

¥The earliest method of registering event handlers found on the Web involved event handler HTML attributes (or inline event handlers) like the one shown above — the attribute value is literally the JavaScript code you want to run when the event occurs. The above example invokes a function defined inside a <script> element on the same page, but you could also insert JavaScript directly inside the attribute, for example:

html
<button onclick="alert('Hello, this is my old-fashioned event handler!');">
  Press me
</button>

你可以找到许多事件处理程序属性的 HTML 属性等效项;然而,你不应该使用它们 - 它们被认为是不好的做法。如果你做的事情非常快,那么使用事件处理程序属性似乎很容易,但它们很快就会变得难以管理且效率低下。

¥You can find HTML attribute equivalents for many of the event handler properties; however, you shouldn't use these — they are considered bad practice. It might seem easy to use an event handler attribute if you are doing something really quick, but they quickly become unmanageable and inefficient.

首先,混合 HTML 和 JavaScript 并不是一个好主意,因为它会变得难以阅读。将 JavaScript 保持独立是一个很好的做法,如果它位于单独的文件中,你可以将其应用到多个 HTML 文档。

¥For a start, it is not a good idea to mix up your HTML and your JavaScript, as it becomes hard to read. Keeping your JavaScript separate is a good practice, and if it is in a separate file you can apply it to multiple HTML documents.

即使在单个文件中,内联事件处理程序也不是一个好主意。一个按钮没问题,但如果有 100 个按钮怎么办?你必须向该文件添加 100 个属性;它很快就会变成维护噩梦。使用 JavaScript,你可以轻松地将事件处理函数添加到页面上的所有按钮,无论有多少个,使用如下所示:

¥Even in a single file, inline event handlers are not a good idea. One button is OK, but what if you had 100 buttons? You'd have to add 100 attributes to the file; it would quickly turn into a maintenance nightmare. With JavaScript, you could easily add an event handler function to all the buttons on the page no matter how many there were, using something like this:

js
const buttons = document.querySelectorAll("button");

for (const button of buttons) {
  button.addEventListener("click", bgChange);
}

最后,作为一种安全措施,许多常见的服务器配置将不允许内联 JavaScript。

¥Finally, many common server configurations will disallow inline JavaScript, as a security measure.

你永远不应该使用 HTML 事件处理程序属性 - 这些属性已经过时,并且使用它们是不好的做法。

¥You should never use the HTML event handler attributes — those are outdated, and using them is bad practice.

事件对象

¥Event objects

有时,在事件处理函数内,你会看到用 eventevte 等名称指定的参数。这称为事件对象,它会自动传递给事件处理程序以提供额外的功能和信息。例如,让我们稍微重写一下随机颜色示例:

¥Sometimes, inside an event handler function, you'll see a parameter specified with a name such as event, evt, or e. This is called the event object, and it is automatically passed to event handlers to provide extra features and information. For example, let's rewrite our random color example again slightly:

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

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

function bgChange(e) {
  const rndCol = `rgb(${random(255)} ${random(255)} ${random(255)})`;
  e.target.style.backgroundColor = rndCol;
  console.log(e);
}

btn.addEventListener("click", bgChange);

注意:你可以在 GitHub 上找到此示例的 完整源代码(也可以找到 看到它实时运行)。

¥Note: You can find the full source code for this example on GitHub (also see it running live).

在这里,你可以看到我们在函数中包含了一个事件对象 e,并在函数中设置了 e.target 上的背景颜色样式(即按钮本身)。事件对象的 target 属性始终是对发生事件的元素的引用。因此,在本示例中,我们在按钮而不是页面上设置随机背景颜色。

¥Here you can see we are including an event object, e, in the function, and in the function setting a background color style on e.target — which is the button itself. The target property of the event object is always a reference to the element the event occurred upon. So, in this example, we are setting a random background color on the button, not the page.

注意:请参阅下面的 事件委托 部分,了解我们使用 event.target 的示例。

¥Note: See the Event delegation section below for an example where we use event.target.

注意:你可以为事件对象使用任何你喜欢的名称 - 你只需选择一个名称,然后可以使用该名称在事件处理程序函数中引用它。e/evt/event 是开发者最常用的,因为它们简短且易于记忆。保持一致总是好的 - 与自己保持一致,如果可能的话与他人保持一致。

¥Note: You can use any name you like for the event object — you just need to choose a name that you can then use to reference it inside the event handler function. e/evt/event is most commonly used by developers because they are short and easy to remember. It's always good to be consistent — with yourself, and with others if possible.

事件对象的额外属性

¥Extra properties of event objects

大多数事件对象都有一组可用于事件对象的标准属性和方法;有关完整列表,请参阅 Event 对象参考。

¥Most event objects have a standard set of properties and methods available on the event object; see the Event object reference for a full list.

某些事件对象添加与该特定事件类型相关的额外属性。例如,当用户按下某个键时,会触发 keydown 事件。它的事件对象是 KeyboardEvent,它是一个专门的 Event 对象,具有 key 属性,告诉你按下了哪个键:

¥Some event objects add extra properties that are relevant to that particular type of event. For example, the keydown event fires when the user presses a key. Its event object is a KeyboardEvent, which is a specialized Event object with a key property that tells you which key was pressed:

html
<input id="textBox" type="text" />
<div id="output"></div>
js
const textBox = document.querySelector("#textBox");
const output = document.querySelector("#output");
textBox.addEventListener("keydown", (event) => {
  output.textContent = `You pressed "${event.key}".`;
});
css
div {
  margin: 0.5rem 0;
}

尝试在文本框中输入内容并查看输出:

¥Try typing into the text box and see the output:

防止默认行为

¥Preventing default behavior

有时,你会遇到这样的情况:你希望阻止某个事件执行其默认操作。最常见的示例是 Web 表单,例如自定义注册表单。当你填写详细信息并单击提交按钮时,自然的行为是将数据提交到服务器上的指定页面进行处理,并将浏览器重定向到某种 "成功消息" 页面(或同一页面) ,如果未指定另一个)。

¥Sometimes, you'll come across a situation where you want to prevent an event from doing what it does by default. The most common example is that of a web form, for example, a custom registration form. When you fill in the details and click the submit button, the natural behavior is for the data to be submitted to a specified page on the server for processing, and the browser to be redirected to a "success message" page of some kind (or the same page, if another is not specified).

当用户没有正确提交数据时,麻烦就会出现 - 作为开发者,你希望阻止向服务器提交数据,并给出一条错误消息,说明出了什么问题以及需要采取哪些措施来纠正错误。某些浏览器支持自动表单数据验证功能,但由于许多浏览器不支持,因此建议你不要依赖这些功能并实现自己的验证检查。让我们看一个简单的例子。

¥The trouble comes when the user has not submitted the data correctly — as a developer, you want to prevent the submission to the server and give an error message saying what's wrong and what needs to be done to put things right. Some browsers support automatic form data validation features, but since many don't, you are advised to not rely on those and implement your own validation checks. Let's look at a simple example.

首先,一个简单的 HTML 表单,要求你输入你的名字和姓氏:

¥First, a simple HTML form that requires you to enter your first and last name:

html
<form>
  <div>
    <label for="fname">First name: </label>
    <input id="fname" type="text" />
  </div>
  <div>
    <label for="lname">Last name: </label>
    <input id="lname" type="text" />
  </div>
  <div>
    <input id="submit" type="submit" />
  </div>
</form>
<p></p>
css
div {
  margin-bottom: 10px;
}

现在是一些 JavaScript - 这里我们在 submit 事件的处理程序中实现了一个非常简单的检查(表单提交时会触发提交事件),以测试文本字段是否为空。如果是,我们在事件对象上调用 preventDefault() 函数 - 这会停止表单提交 - 然后在表单下面的段落中显示一条错误消息,告诉用户出了什么问题:

¥Now some JavaScript — here we implement a very simple check inside a handler for the submit event (the submit event is fired on a form when it is submitted) that tests whether the text fields are empty. If they are, we call the preventDefault() function on the event object — which stops the form submission — and then display an error message in the paragraph below our form to tell the user what's wrong:

js
const form = document.querySelector("form");
const fname = document.getElementById("fname");
const lname = document.getElementById("lname");
const para = document.querySelector("p");

form.addEventListener("submit", (e) => {
  if (fname.value === "" || lname.value === "") {
    e.preventDefault();
    para.textContent = "You need to fill in both names!";
  }
});

显然,这是相当弱的表单验证 - 例如,它不会阻止用户使用在字段中输入的空格或数字来验证表单 - 但出于示例目的,这是可以的。输出如下:

¥Obviously, this is pretty weak form validation — it wouldn't stop the user from validating the form with spaces or numbers entered into the fields, for example — but it is OK for example purposes. The output is as follows:

注意:有关完整的源代码,请参阅 preventdefault-validation.html(另请参阅此处的 实时运行)。

¥Note: For the full source code, see preventdefault-validation.html (also see it running live here).

事件冒泡

¥Event bubbling

事件冒泡描述了浏览器如何处理针对嵌套元素的事件。

¥Event bubbling describes how the browser handles events targeted at nested elements.

在父元素上设置监听器

¥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.

这不仅仅是网页

¥It's not just web pages

事件并不是 JavaScript 所独有的 - 大多数编程语言都有某种事件模型,并且该模型的工作方式通常与 JavaScript 的方式不同。事实上,网页 JavaScript 中的事件模型与其他环境中使用的 JavaScript 事件模型不同。

¥Events are not unique to JavaScript — most programming languages have some kind of event model, and the way the model works often differs from JavaScript's way. In fact, the event model in JavaScript for web pages differs from the event model for JavaScript as it is used in other environments.

例如,Node.js 是一个非常流行的 JavaScript 运行时,它使开发者能够使用 JavaScript 来构建网络和服务器端应用。Node.js 事件模型 依靠监听器来监听事件,并依靠触发器定期触发事件 - 听起来并没有什么不同,但代码却截然不同,利用 on() 等函数来注册事件监听器,使用 once() 来注册事件监听器 运行一次后取消注册。HTTP 连接事件文档 就是一个很好的例子。

¥For example, Node.js is a very popular JavaScript runtime that enables developers to use JavaScript to build network and server-side applications. The Node.js event model relies on listeners to listen for events and emitters to emit events periodically — it doesn't sound that different, but the code is quite different, making use of functions like on() to register an event listener, and once() to register an event listener that unregisters after it has run once. The HTTP connect event docs provide a good example.

你还可以使用 JavaScript 来构建跨浏览器附加组件 - 浏览器功能增强 - 使用一种名为 WebExtensions 的技术。事件模型与 Web 事件模型类似,但有一点不同 - 事件监听器的属性是用 camel case 编写的(例如 onMessage 而不是 onmessage),并且需要与 addListener 函数结合。有关示例,请参阅 runtime.onMessage 页。

¥You can also use JavaScript to build cross-browser add-ons — browser functionality enhancements — using a technology called WebExtensions. The event model is similar to the web events model, but a bit different — event listeners' properties are written in camel case (such as onMessage rather than onmessage), and need to be combined with the addListener function. See the runtime.onMessage page for an example.

在学习的这个阶段,你不需要了解有关其他此类环境的任何信息;你只需了解其他环境即可。我们只是想明确指出,事件在不同的编程环境中可能会有所不同。

¥You don't need to understand anything about other such environments at this stage in your learning; we just wanted to make it clear that events can differ in different programming environments.

测试你的技能!

¥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 的一篇非常详细的文章。
  • 事件访问(事件对象的讨论) - Peter-Paul Koch 的另一篇非常详细的文章。