EventTarget:addEventListener() 方法

Baseline 广泛可用 *

此特性已相当成熟,可在许多设备和浏览器版本上使用。自 ⁨2015 年 7 月⁩以来,各浏览器均已提供此特性。

* 此特性的某些部分可能存在不同级别的支持。

注意:此功能在 Web Workers 中可用。

EventTarget 接口的 addEventListener() 方法设置一个函数,该函数将在每次指定的事件被分派到目标时被调用。

常见的事件目标是 Element 或其子元素、DocumentWindow,但目标可以是任何支持事件的对象(例如 IDBRequest)。

注意: addEventListener() 方法是注册事件监听器的推荐方式。其优点如下:

  • 它允许为一个事件添加多个处理程序。这对于需要与其他库或扩展良好协作的库、JavaScript 模块或任何其他类型的代码尤其有用。
  • 与使用 onXYZ 属性相比,它让您可以对监听器激活的阶段(捕获阶段与冒泡阶段)进行更精细的控制。
  • 它适用于任何事件目标,而不仅仅是 HTML 或 SVG 元素。

addEventListener() 方法通过将一个函数或一个实现 handleEvent() 函数的对象添加到调用它的 EventTarget 上指定事件类型的事件监听器列表中来工作。如果该函数或对象已经在此目标的事件监听器列表中,则该函数或对象不会被第二次添加。

注意: 如果一个特定的匿名函数已经注册到某个目标的事件监听器列表中,然后代码后面在 addEventListener 调用中给出了一个相同的匿名函数,那么第二个函数会被添加到该目标的事件监听器列表中。

实际上,即使使用相同的、重复调用的不变源代码定义,匿名函数也不是相同的,即使在循环中也是如此

在这种情况下重复定义相同的匿名函数可能会出现问题。(请参阅下面的内存问题。)

如果在另一个监听器内部将事件监听器添加到 EventTarget 中——也就是说,在事件处理过程中——该事件将不会触发新的监听器。但是,新的监听器可能会在事件流的后期阶段被触发,例如在冒泡阶段。

语法

js
addEventListener(type, listener)
addEventListener(type, listener, options)
addEventListener(type, listener, useCapture)

参数

type

一个区分大小写的字符串,表示要监听的事件类型

监听器

当指定类型的事件发生时接收通知的对象(实现 Event 接口的对象)。这必须是 null、具有 handleEvent() 方法的对象或 JavaScript 函数。有关回调本身的详细信息,请参阅事件监听器回调

options 可选

一个指定事件监听器特性的对象。可用选项如下:

capture 可选

一个布尔值,指示此类型的事件是否将在 DOM 树中任何其下的 EventTarget 收到事件之前分派到已注册的 listener。如果未指定,则默认为 false

once 可选

一个布尔值,指示 listener 在添加后最多应调用一次。如果为 true,则 listener 在调用后将自动删除。如果未指定,则默认为 false

passive 可选

一个布尔值,如果为 true,则表示 listener 指定的函数永远不会调用 preventDefault()。如果一个被动监听器调用 preventDefault(),则不会发生任何事情,并且可能会生成控制台警告。

如果未指定此选项,则默认为 false——但在 Safari 之外的浏览器中,对于 wheelmousewheeltouchstarttouchmove 事件,它默认为 true。有关更多信息,请参阅使用被动监听器

signal 可选

一个 AbortSignal。当拥有 AbortSignalAbortControllerabort() 方法被调用时,监听器将被移除。如果未指定,则没有 AbortSignal 与监听器关联。

useCapture 可选

一个布尔值,指示此类型的事件是否将在 DOM 树中任何其下的 EventTarget 收到事件之前分派到已注册的 listener。通过树向上冒泡的事件将不会触发指定使用捕获的监听器。事件冒泡和捕获是当一个元素嵌套在另一个元素中,并且两个元素都已注册该事件的处理程序时,传播事件的两种方式。事件传播模式决定了元素接收事件的顺序。有关详细说明,请参阅 DOM 规范JavaScript 事件顺序。如果未指定,useCapture 默认为 false

注意: 对于附加到事件目标的事件监听器,事件处于目标阶段,而不是捕获和冒泡阶段。处于捕获阶段的事件监听器在目标和冒泡阶段的事件监听器之前被调用。

wantsUntrusted 可选 非标准

一个 Firefox (Gecko) 特定的参数。如果为 true,监听器接收由 Web 内容分派的合成事件(对于浏览器 chrome 默认为 false,对于常规网页默认为 true)。此参数对于附加组件中的代码以及浏览器本身很有用。

返回值

无(undefined)。

用法说明

事件监听器回调

事件监听器可以指定为回调函数,也可以指定为具有 handleEvent() 方法作为回调函数的对象。

回调函数本身具有与 handleEvent() 方法相同的参数和返回值;也就是说,回调接受一个参数:一个基于 Event 的对象,描述已发生的事件,并且它不返回任何内容。

例如,一个可以用于处理 fullscreenchangefullscreenerror 的事件处理程序回调可能如下所示:

js
function handleEvent(event) {
  if (event.type === "fullscreenchange") {
    /* handle a full screen toggle */
  } else {
    /* handle a full screen toggle error */
  }
}

处理程序中“this”的值

通常需要引用触发事件处理程序的元素,例如在使用通用处理程序处理一组相似元素时。

使用 addEventListener() 将处理程序函数附加到元素时,处理程序内部的 this 值将是对元素的引用。它将与传递给处理程序的事件参数的 currentTarget 属性的值相同。

js
my_element.addEventListener("click", function (e) {
  console.log(this.className); // logs the className of my_element
  console.log(e.currentTarget === this); // logs `true`
});

提醒一下,箭头函数没有自己的 this 上下文

js
my_element.addEventListener("click", (e) => {
  console.log(this.className); // WARNING: `this` is not `my_element`
  console.log(e.currentTarget === this); // logs `false`
});

如果在 HTML 源中的元素上指定了事件处理程序(例如,onclick),则属性值中的 JavaScript 代码实际上被包装在一个处理程序函数中,该函数以与 addEventListener() 一致的方式绑定 this 的值;代码中出现的 this 表示对元素的引用。

html
<table id="my_table" onclick="console.log(this.id);">
  <!-- `this` refers to the table; logs 'my_table' -->
  …
</table>

请注意,属性值中的代码调用的函数内部的 this 值按照标准规则行为。这在以下示例中显示:

html
<script>
  function logID() {
    console.log(this.id);
  }
</script>
<table id="my_table" onclick="logID();">
  <!-- when called, `this` will refer to the global object -->
  …
</table>

logID() 中的 this 值是对全局对象 Window 的引用(在严格模式下为 undefined)。

使用 bind() 指定“this”

Function.prototype.bind() 方法允许您为所有后续调用建立一个固定的 this 上下文——绕过根据调用函数的上下文导致 this 不明确的问题。但是,请注意,您需要保留对监听器的引用,以便以后可以将其删除。

这是一个有和没有 bind() 的示例:

js
class Something {
  name = "Something Good";
  constructor(element) {
    // bind causes a fixed `this` context to be assigned to `onclick2`
    this.onclick2 = this.onclick2.bind(this);
    element.addEventListener("click", this.onclick1);
    element.addEventListener("click", this.onclick2); // Trick
  }
  onclick1(event) {
    console.log(this.name); // undefined, as `this` is the element
  }
  onclick2(event) {
    console.log(this.name); // 'Something Good', as `this` is bound to the Something instance
  }
}

const s = new Something(document.body);

另一种解决方案是使用一个名为 handleEvent() 的特殊函数来捕获任何事件:

js
class Something {
  name = "Something Good";
  constructor(element) {
    // Note that the listeners in this case are `this`, not this.handleEvent
    element.addEventListener("click", this);
    element.addEventListener("dblclick", this);
  }
  handleEvent(event) {
    console.log(this.name); // 'Something Good', as this is bound to newly created object
    switch (event.type) {
      case "click":
        // some code here…
        break;
      case "dblclick":
        // some code here…
        break;
    }
  }
}

const s = new Something(document.body);

处理 this 引用的另一种方法是使用箭头函数,它不会创建单独的 this 上下文。

js
class SomeClass {
  name = "Something Good";

  register() {
    window.addEventListener("keydown", (e) => {
      this.someMethod(e);
    });
  }

  someMethod(e) {
    console.log(this.name);
    switch (e.code) {
      case "ArrowUp":
        // some code here…
        break;
      case "ArrowDown":
        // some code here…
        break;
    }
  }
}

const myObject = new SomeClass();
myObject.register();

将数据传入和传出事件监听器

事件监听器只接受一个参数,即一个 EventEvent 的子类,该参数会自动传递给监听器,并且返回值被忽略。因此,要将数据传入和传出事件监听器,您需要创建闭包,而不是通过参数和返回值传递数据。

作为事件监听器传递的函数可以访问包含该函数的所有外部作用域中声明的变量。

js
const myButton = document.getElementById("my-button-id");
let someString = "Data";

myButton.addEventListener("click", () => {
  console.log(someString);
  // 'Data' on first click,
  // 'Data Again' on second click

  someString = "Data Again";
});

console.log(someString); // Expected Value: 'Data' (will never output 'Data Again')

阅读函数指南以获取有关函数作用域的更多信息。

内存问题

js
const elems = document.getElementsByTagName("*");

// Case 1
for (const elem of elems) {
  elem.addEventListener("click", (e) => {
    // Do something
  });
}

// Case 2
function processEvent(e) {
  // Do something
}

for (const elem of elems) {
  elem.addEventListener("click", processEvent);
}

在上面的第一种情况下,每次循环迭代都会创建一个新的(匿名)处理程序函数。在第二种情况下,使用相同的先前声明的函数作为事件处理程序,这导致更小的内存消耗,因为只创建一个处理程序函数。此外,在第一种情况下,无法调用 removeEventListener(),因为没有保留对匿名函数的引用(或在此处,没有保留对循环可能创建的多个匿名函数中的任何一个的引用)。在第二种情况下,可以执行 myElement.removeEventListener("click", processEvent, false),因为 processEvent 是函数引用。

实际上,关于内存消耗,缺乏函数引用并不是真正的问题;更确切地说,是缺乏静态函数引用。

使用被动监听器

如果事件有默认行为——例如,默认情况下滚动容器的 wheel 事件——浏览器通常无法在事件监听器完成之前开始默认行为,因为它不预先知道事件监听器是否会通过调用 Event.preventDefault() 来取消默认行为。如果事件监听器执行时间过长,这可能会导致在执行默认行为之前出现明显的延迟,也称为卡顿

通过将 passive 选项设置为 true,事件监听器声明它不会取消默认行为,因此浏览器可以立即开始默认行为,而无需等待监听器完成。如果监听器随后调用 Event.preventDefault(),则不会产生任何效果。

addEventListener() 的规范将 passive 选项的默认值定义为始终为 false。然而,为了在旧代码中实现被动监听器的滚动性能优势,现代浏览器已将 passive 选项的默认值更改为 true,用于文档级节点 WindowDocumentDocument.body 上的 wheelmousewheeltouchstarttouchmove 事件。这可以防止事件监听器取消事件,因此它不会在用户滚动时阻塞页面渲染。

正因为如此,当您想覆盖该行为并确保 passive 选项为 false 时,您必须显式地将该选项设置为 false(而不是依赖默认值)。

您无需担心基本 scroll 事件的 passive 值。由于它无法取消,事件监听器无论如何都无法阻塞页面渲染。

有关显示被动监听器效果的示例,请参阅使用被动监听器提高滚动性能

示例

添加一个简单的监听器

此示例演示如何使用 addEventListener() 监听元素的鼠标点击。

HTML

html
<table id="outside">
  <tr>
    <td id="t1">one</td>
  </tr>
  <tr>
    <td id="t2">two</td>
  </tr>
</table>

JavaScript

js
// Function to change the content of t2
function modifyText() {
  const t2 = document.getElementById("t2");
  const isNodeThree = t2.firstChild.nodeValue === "three";
  t2.firstChild.nodeValue = isNodeThree ? "two" : "three";
}

// Add event listener to table
const el = document.getElementById("outside");
el.addEventListener("click", modifyText);

在此代码中,modifyText() 是使用 addEventListener() 注册的 click 事件的监听器。表中任何位置的点击都会冒泡到处理程序并运行 modifyText()

结果

添加一个可中止的监听器

此示例演示如何添加一个可以使用 AbortSignal 中止的 addEventListener()

HTML

html
<table id="outside">
  <tr>
    <td id="t1">one</td>
  </tr>
  <tr>
    <td id="t2">two</td>
  </tr>
</table>

JavaScript

js
// Add an abortable event listener to table
const controller = new AbortController();
const el = document.getElementById("outside");
el.addEventListener("click", modifyText, { signal: controller.signal });

// Function to change the content of t2
function modifyText() {
  const t2 = document.getElementById("t2");
  if (t2.firstChild.nodeValue === "three") {
    t2.firstChild.nodeValue = "two";
  } else {
    t2.firstChild.nodeValue = "three";
    controller.abort(); // remove listener after value reaches "three"
  }
}

在上面的示例中,我们修改了前一个示例中的代码,使得第二行的内容更改为“three”后,我们调用了我们传递给 addEventListener() 调用的 AbortControllerabort()。这导致值永远保持为“three”,因为我们不再有任何代码监听点击事件。

结果

带有匿名函数的事件监听器

在这里,我们将了解如何使用匿名函数将参数传递给事件监听器。

HTML

html
<table id="outside">
  <tr>
    <td id="t1">one</td>
  </tr>
  <tr>
    <td id="t2">two</td>
  </tr>
</table>

JavaScript

js
// Function to change the content of t2
function modifyText(newText) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = newText;
}

// Function to add event listener to table
const el = document.getElementById("outside");
el.addEventListener("click", function () {
  modifyText("four");
});

请注意,监听器是一个匿名函数,它封装了代码,然后该代码又能够将参数发送到 modifyText() 函数,该函数负责实际响应事件。

结果

带有箭头函数的事件监听器

此示例演示了使用箭头函数语法实现的事件监听器。

HTML

html
<table id="outside">
  <tr>
    <td id="t1">one</td>
  </tr>
  <tr>
    <td id="t2">two</td>
  </tr>
</table>

JavaScript

js
// Function to change the content of t2
function modifyText(newText) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = newText;
}

// Add event listener to table with an arrow function
const el = document.getElementById("outside");
el.addEventListener("click", () => {
  modifyText("four");
});

结果

请注意,虽然匿名函数和箭头函数相似,但它们具有不同的 this 绑定。匿名函数(以及所有传统的 JavaScript 函数)创建自己的 this 绑定,而箭头函数继承包含函数的 this 绑定。

这意味着包含函数可用的变量和常量在使用箭头函数时也适用于事件处理程序。

选项使用示例

HTML

html
<div class="outer">
  outer, once & none-once
  <div class="middle" target="_blank">
    middle, capture & none-capture
    <a class="inner1" href="https://www.mozilla.org" target="_blank">
      inner1, passive & preventDefault(which is not allowed)
    </a>
    <a class="inner2" href="https://mdn.org.cn/" target="_blank">
      inner2, none-passive & preventDefault(not open new page)
    </a>
  </div>
</div>
<hr />
<button class="clear-button">Clear logs</button>
<section class="demo-logs"></section>

CSS

css
.outer,
.middle,
.inner1,
.inner2 {
  display: block;
  width: 520px;
  padding: 15px;
  margin: 15px;
  text-decoration: none;
}
.outer {
  border: 1px solid red;
  color: red;
}
.middle {
  border: 1px solid green;
  color: green;
  width: 460px;
}
.inner1,
.inner2 {
  border: 1px solid purple;
  color: purple;
  width: 400px;
}

JavaScript

js
const outer = document.querySelector(".outer");
const middle = document.querySelector(".middle");
const inner1 = document.querySelector(".inner1");
const inner2 = document.querySelector(".inner2");

const capture = {
  capture: true,
};
const noneCapture = {
  capture: false,
};
const once = {
  once: true,
};
const noneOnce = {
  once: false,
};
const passive = {
  passive: true,
};
const nonePassive = {
  passive: false,
};

outer.addEventListener("click", onceHandler, once);
outer.addEventListener("click", noneOnceHandler, noneOnce);
middle.addEventListener("click", captureHandler, capture);
middle.addEventListener("click", noneCaptureHandler, noneCapture);
inner1.addEventListener("click", passiveHandler, passive);
inner2.addEventListener("click", nonePassiveHandler, nonePassive);

function onceHandler(event) {
  log("outer, once");
}
function noneOnceHandler(event) {
  log("outer, none-once, default\n");
}
function captureHandler(event) {
  // event.stopImmediatePropagation();
  log("middle, capture");
}
function noneCaptureHandler(event) {
  log("middle, none-capture, default");
}
function passiveHandler(event) {
  // Unable to preventDefault inside passive event listener invocation.
  event.preventDefault();
  log("inner1, passive, open new page");
}
function nonePassiveHandler(event) {
  event.preventDefault();
  // event.stopPropagation();
  log("inner2, none-passive, default, not open new page");
}

结果

分别点击外部、中部、内部容器,查看选项的工作原理。

带有多个选项的事件监听器

您可以在 options 参数中设置多个选项。在以下示例中,我们设置了两个选项:

  • passive,断言处理程序不会调用 preventDefault()
  • once,确保事件处理程序只会被调用一次。

HTML

html
<button id="example-button">You have not clicked this button.</button>
<button id="reset-button">Click this button to reset the first button.</button>

JavaScript

js
const buttonToBeClicked = document.getElementById("example-button");

const resetButton = document.getElementById("reset-button");

// the text that the button is initialized with
const initialText = buttonToBeClicked.textContent;

// the text that the button contains after being clicked
const clickedText = "You have clicked this button.";

// we hoist the event listener callback function
// to prevent having duplicate listeners attached
function eventListener() {
  buttonToBeClicked.textContent = clickedText;
}

function addListener() {
  buttonToBeClicked.addEventListener("click", eventListener, {
    passive: true,
    once: true,
  });
}

// when the reset button is clicked, the example button is reset,
// and allowed to have its state updated again
resetButton.addEventListener("click", () => {
  buttonToBeClicked.textContent = initialText;
  addListener();
});

addListener();

结果

使用被动监听器提高滚动性能

以下示例显示了设置 passive 的效果。它包含一个 <div>,其中包含一些文本和一个复选框。

HTML

html
<div id="container">
  <p>
    But down there it would be dark now, and not the lovely lighted aquarium she
    imagined it to be during the daylight hours, eddying with schools of tiny,
    delicate animals floating and dancing slowly to their own serene currents
    and creating the look of a living painting. That was wrong, in any case. The
    ocean was different from an aquarium, which was an artificial environment.
    The ocean was a world. And a world is not art. Dorothy thought about the
    living things that moved in that world: large, ruthless and hungry. Like us
    up here.
  </p>
</div>

<div>
  <input type="checkbox" id="passive" name="passive" checked />
  <label for="passive">passive</label>
</div>

JavaScript

代码向容器的 wheel 事件添加了一个监听器,该事件默认会滚动容器。监听器运行一个长时间运行的操作。最初,监听器是使用 passive 选项添加的,并且每当复选框被切换时,代码就会切换 passive 选项。

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

passive.addEventListener("change", (event) => {
  container.removeEventListener("wheel", wheelHandler);
  container.addEventListener("wheel", wheelHandler, {
    passive: passive.checked,
    once: true,
  });
});

container.addEventListener("wheel", wheelHandler, {
  passive: true,
  once: true,
});

function wheelHandler() {
  function isPrime(n) {
    for (let c = 2; c <= Math.sqrt(n); ++c) {
      if (n % c === 0) {
        return false;
      }
    }
    return true;
  }

  const quota = 1000000;
  const primes = [];
  const maximum = 1000000;

  while (primes.length < quota) {
    const candidate = Math.floor(Math.random() * (maximum + 1));
    if (isPrime(candidate)) {
      primes.push(candidate);
    }
  }

  console.log(primes);
}

结果

其效果是:

  • 最初,监听器是被动的,因此尝试用滚轮滚动容器是即时的。
  • 如果取消选中“passive”并尝试使用滚轮滚动容器,则在容器滚动之前会有明显的延迟,因为浏览器必须等待长时间运行的监听器完成。

规范

规范
DOM
# ref-for-dom-eventtarget-addeventlistener③

浏览器兼容性

另见