javascriptevent-handling

Why does adding an event handler during a handler appear to work differently from removing one?


I'm writing an event driver for a project I'm working on and I'm using the standard browser event system as a model, which has made me look at it in more detail. I'm testing things in Edge and Firefox.

Consider this:

document.documentElement.addEventListener("click", () =>
{
    console.log("Handler 1");
    document.documentElement.addEventListener("click", () => console.log("Handler 2"));

    return;
});

When the document element is clicked for the first time, the output in the console is:

Handler 1

From this, it would seem that, even though the end result are two events bound in order (Handler 1 and Handler 2), a handler added inside another event handler is not added to the currently processed queue.

Now consider this:

const handler2 = () => console.log("Handler 2");

document.documentElement.addEventListener("click", () =>
{
    console.log("Handler 1");
    document.documentElement.removeEventListener("click", handler2);

    return;
});
document.documentElement.addEventListener("click", handler2);

When the document element is clicked, the output in the console is also:

Handler 1

In this second case, the two handlers are initially bound in order, and I would presume the event queue contains both when Handler 1 is initially run, but removing an event handler is enough to also remove it from the currently processed queue.

Is there a rationale behind this apparent difference? I don't understand why removing the listener would impact the event queue. If the browser doesn't update the queue when an event is added for performance reasons, then surely those reasons apply when removing too. I would also think the logical thing would be to have the two operations behave consistently.


Solution

  • The difference in the behavior between event listener registration and removal is spelled out in the DOM specification:

    I can imagine the following rationale behind this: It shall be possible to remove an event listener through an abort signal:

    const a = new AbortController()
    b.addEventListener("click", function() {
      alert(1);
      a.abort();
    });
    b.addEventListener("click", function() {
      alert(2);
    }, {
      signal: a.signal
    });
    <button id="b">Click</button>

    The first event listener aborts the second and this must have an effect even though the event has already been dispatched.

    Here is the discussion from 2015 when this behavior was written into the specification.