EventTarget: addEventListener() 方法

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

addEventListener() 方法是 EventTarget 接口的方法,用于设置一个函数,该函数将在指定事件传递到目标时被调用。

常见的目标包括 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

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

listener

当发生指定类型的事件时,接收通知的对象(一个实现了 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。当给定的 AbortSignal 对象的 abort() 方法被调用时,将移除该监听器。如果未指定,则没有 AbortSignal 与监听器关联。

useCapture 可选

一个布尔值,指示此类型的事件是否将在分派到 DOM 树中其下方的任何 EventTarget 之前分派到注册的 listener。向上冒泡穿过树的事件不会触发指定为使用捕获的监听器。事件冒泡和捕获是当嵌套在另一个元素中的元素中发生事件时,两个元素都为该事件注册了处理程序时,传播事件的两种方式。事件传播模式决定了元素接收事件的顺序。有关详细说明,请参阅DOM Level 3 EventsJavaScript 事件顺序。如果未指定,则 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 */
  }
}

安全地检测选项支持

在较旧版本的 DOM 规范中,addEventListener() 的第三个参数是一个布尔值,指示是否使用捕获。随着时间的推移,很明显需要更多选项。与其向函数添加更多参数(在处理可选值时会使事情变得异常复杂),不如将第三个参数更改为一个对象,该对象可以包含定义配置删除事件监听器过程的选项值的各种属性。

因为较旧的浏览器(以及一些不太旧的浏览器)仍然假设第三个参数是布尔值,所以您需要构建您的代码以智能地处理这种情况。您可以通过对您感兴趣的每个选项使用特性检测来做到这一点。

例如,如果您想检查 passive 选项:

js
let passiveSupported = false;

try {
  const options = {
    get passive() {
      // This function will be called when the browser
      // attempts to access the passive property.
      passiveSupported = true;
      return false;
    },
  };

  window.addEventListener("test", null, options);
  window.removeEventListener("test", null, options);
} catch (err) {
  passiveSupported = false;
}

这会创建一个带有 passive 属性 getter 函数的 options 对象;如果调用 getter 函数,它会将标志 passiveSupported 设置为 true。这意味着,如果浏览器检查 options 对象上 passive 属性的值,则 passiveSupported 将被设置为 true;否则,它将保持 false。然后,我们调用 addEventListener() 来设置一个假的事件处理程序,并指定这些选项,以便如果浏览器识别对象作为第三个参数,则会检查这些选项。然后,我们调用 removeEventListener() 来清理我们的操作。(请注意,对于未调用的事件侦听器,handleEvent() 会被忽略。)

您可以通过这种方式检查任何选项是否受支持。只需使用类似于上面显示的代码为该选项添加一个 getter 函数。

然后,当您想要创建一个实际使用相关选项的事件侦听器时,您可以执行以下操作

js
someElement.addEventListener(
  "mouseup",
  handleMouseUp,
  passiveSupported ? { passive: true } : false,
);

在这里,我们为元素 someElement 上的 mouseup 事件添加了一个侦听器。对于第三个参数,如果 passiveSupportedtrue,我们指定一个 passive 设置为 trueoptions 对象;否则,我们知道需要传递一个布尔值,并将 false 作为 useCapture 参数的值传递。

您可以在 实现特性检测 文档和关于 EventListenerOptions 的解释器中了解更多信息,该解释器来自 Web Incubator 社区组

处理程序内部的“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, false);
    element.addEventListener("click", this.onclick2, false); // 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, false);
    element.addEventListener("dblclick", this, false);
  }
  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 elts = document.getElementsByTagName("*");

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

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

for (const elt of elts) {
  elt.addEventListener("click", processEvent, false);
}

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

实际上,关于内存消耗,缺乏保留函数引用并不是真正的问题;而是缺乏保留静态函数引用。

使用被动侦听器

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

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

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

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

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

请参阅 使用被动侦听器提高滚动性能,了解显示被动侦听器效果的示例。

旧版浏览器

在不支持 addEventListener()options 参数的旧版浏览器中,尝试使用它会阻止使用 useCapture 参数,除非正确使用了 特性检测

示例

添加一个简单的侦听器

此示例演示如何使用 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, false);

在此代码中,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() 调用的 AbortController 中调用 abort()。这会导致值永远保持为“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(new_text) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = new_text;
}

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

请注意,侦听器是一个匿名函数,它封装了随后能够将参数发送到 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(new_text) {
  const t2 = document.getElementById("t2");
  t2.firstChild.nodeValue = new_text;
}

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

结果

请注意,虽然匿名函数和箭头函数相似,但它们的 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 对象中使用特定值之前,最好确保用户的浏览器支持它,因为这些是并非所有浏览器在历史上都支持的补充。有关详细信息,请参阅 安全检测选项支持

具有多个选项的事件侦听器

您可以在 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");
passive.addEventListener("change", (event) => {
  container.removeEventListener("wheel", wheelHandler);
  container.addEventListener("wheel", wheelHandler, {
    passive: passive.checked,
    once: true,
  });
});

const container = document.querySelector("#container");
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③

浏览器兼容性

BCD 表格仅在浏览器中加载

另请参阅