Как addEventListener однозначно идентифицирует переданные ему функции?

#javascript #dom-events

#javascript #dom-события

Вопрос:

Допустим, у меня есть такой код:

 let myFunc = () => {
    console.log("hello");
}
 
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);
document.addEventListener("click", myFunc);  

Почему при нажатии на документ консоль регистрируется только один раз? Я не возражаю против такого поведения, но мне просто любопытно, как это реализовано.

Например, если вы сделали что-то вроде этого:

 let events = {};

function addEventListener(key, callback) {
  if (!key) { return; }

  if (!events.hasOwnProperty(key)) {
    events[key] = {};
  }

  events[key][callback] = callback;
}
  

Тогда вы используете функцию в качестве ключа, но разве для ключей допустимы не только строки? Как JavaScript однозначно идентифицирует функции, чтобы он знал, что не следует добавлять одну и ту же функцию несколько раз?

Ответ №1:

Данный прослушиватель событий с определенной конфигурацией может быть добавлен к элементу только один раз — если вы добавляете его несколько раз, как вы можете видеть, это будет так, как если бы был добавлен только один прослушиватель. Это описано в спецификации здесь:

  1. Если список прослушивателей событий EventTarget не содержит прослушивателя событий, тип которого является типом прослушивателя, обратный вызов — это обратный вызов прослушивателя, а захват — это захват прослушивателя, затем добавьте прослушиватель в список прослушивателей событий EventTarget.

Чтобы расширить это, для слушателя, который считается таким дубликатом:

чей тип является типом слушателя

ссылается на имя события, например 'click'

обратный вызов — это обратный вызов слушателя

это должна быть та же ссылка на функцию ( === на добавленный ранее прослушиватель)

захват — это захват слушателя

относится к тому, слушает ли слушатель на этапе захвата или на этапе пузырьковой обработки. (Это задается третьим логическим параметром addEventListener , который по умолчанию true имеет значение — bubbling , или с { capture: boolean } в качестве третьего аргумента)

Если все вышеперечисленные функции совпадают с функциями прослушивателя, добавленного ранее, то новый прослушиватель будет считаться дубликатом и не будет добавлен снова.


Простой способ добавить такого слушателя несколько раз, если вы хотите, — это выполнить встроенный обратный вызов, который вызывает вашего слушателя:

 let myFunc = () => {
    console.log("hello");
}
 
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());
document.addEventListener("click", () => myFunc());  
 click me  

Вышеуказанное будет работать, потому что обратные вызовы, переданные addEventListener не равны: () => myFunc() не === () => myFunc() является .

Комментарии:

1. Да, мне любопытно, как это реализовано. Как Javascript узнает, что обратный вызов прослушивателя событий уже добавлен? Чтобы уточнить, я не хочу, чтобы функция запускалась 4 раза, я только хочу знать, как они однозначно идентифицируют функции.

2. Смотрите цитату из спецификации в ответе. Если список прослушивателей событий EventTarget не содержит прослушивателя событий, тип которого является типом прослушивателя, обратный вызов — это обратный вызов прослушивателя, а захват — это захват прослушивателя, затем добавьте прослушиватель в список прослушивателей событий EventTarget. Если тот же прослушиватель был добавлен ранее, то в списке прослушивателей событий будет такой идентичный элемент, поэтому новый прослушиватель не будет добавлен.

3. @RyanPeschel Функции являются объектами. Вы можете сравнить их с === . Браузер выполняет это внутренне в addEventListener

4. Таким образом, у них просто есть массив функций, и они просто перебирают существующие, когда вы добавляете новую, чтобы убедиться, что она еще не добавлена? Есть ли какой-либо способ избежать O (n) этого? Я надеялся избежать повторения существующего списка, поэтому я изначально думал об использовании объектов.

5. @RyanPeschel Map также существует, но спецификация W3C для addEventListener существовала задолго до того, Map как стала частью ECMAScript.

Ответ №2:

Концептуально реализация может быть чем-то вроде этого (я игнорирую детали спецификации, не относящиеся к вопросу):

 function addEventListener(type, listener, useCapture = false) {
    let typeListeners = this.eventListeners[type];
    if (!typeListeners) {
        this.eventListeners[type] = [{function: listener, useCapture: useCapture}];
    } else {
        let found = typeListeners.find(l => l.function === listener amp;amp; l.useCapture == useCapture);
        if (!found) {
            typeListeners.push({function: listener, useCapture: useCapture});
        }
    }
}
  

Он выполняет поиск в списке прослушивателей типа события, существующего соответствия функции и useCapture параметров. Если этого еще нет, он добавляет его.

Комментарии:

1. Правильно, здесь используется массив. Возможно ли это сделать с объектами, чтобы вы могли получить извлечение O (1)?

2. Это может быть, но зачем беспокоиться? Необычно иметь несколько прослушивателей для одного и того же события в элементе, и очень редко их так много, что линейный поиск будет проблемой производительности.

3. Я бы ожидал, что в 90% случаев алгоритм хеширования будет дороже, чем линейный.

4. Да, я согласен. Я, вероятно, собираюсь использовать linear, потому что это, вероятно, будет быстрее, а также проще. Мне просто интересно, можно ли однозначно идентифицировать функции в объекте. Похоже, мне нужно будет использовать Map или Set. Приятно знать, спасибо!

5. @RyanPeschel Также необходимо проверить useCapture флаг.