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.club.tw/" 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③

瀏覽器相容性

另見