How does addEventListener uniquely identify the functions passed to it?

Viewed 69

Say I have some code like this:

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

Why does clicking the document only console log once? I don't mind this behavior, but I'm just curious how it is implemented.

For example, if you did something like this:

let events = {};

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

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

  events[key][callback] = callback;
}

Then you're using a function as a key, but aren't only strings valid for keys? How does JavaScript uniquely identify the functions so that it knows not to add the same one multiple times?

2 Answers

A given event listener with a particular configuration can only be added to an element once - if you add it multiple times, as you can see, it will be as if only a single listener was added. This is described in the specification here:

  1. If eventTarget’s event listener list does not contain an event listener whose type is listener’s type, callback is listener’s callback, and capture is listener’s capture, then append listener to eventTarget’s event listener list.

To expand on that, for a listener to be considered such a duplicate:

whose type is listener’s type

refers to the event name, eg 'click'

callback is listener’s callback

which must be the same function reference (=== to a prior listener added)

capture is listener’s capture

refers to whether the listener listens in the capturing phase or the bubbling phase. (This is set by a third boolean parameter to addEventListener, which defaults to true - bubbling, or with { capture: boolean } as the third argument)

If all of the above are the same as that of a listener added previously, then the new listener will be considered a duplicate, and will not be added again.


An easy way to add such a listener multiple times, if you wanted, would be to make an inline callback that calls your listener:

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

The above will work because the callbacks passed to addEventListener are not equal: () => myFunc() is not === to () => myFunc().

The implementation could be something conceptually like this (I'm ignoring the details of the specification not relevant to the question):

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 && l.useCapture == useCapture);
        if (!found) {
            typeListeners.push({function: listener, useCapture: useCapture});
        }
    }
}

It searches the list of listeners for the event type, for an existing match to the function and useCapture parameters. If it's not already there, it adds it.

Related