我会尽量用最抽象的方式来解释。真正的实现可能要复杂得多。因此,我将要使用的名称是假设性的,但它们确实有助于解释事物,我希望 ;)
浏览器中的每个节点都是EventEmitter类的实现  。此类维护一个对象events,其中包含(键)的键:值对eventType:一个包含listener函数(值)的数组。
EventEmitter 类中定义的两个函数是addEventListener和fire。
class EventEmitter {
  constructor(id) {
    this.events = {};
    this.id = id;
  }
  addEventListener(eventType, listener) {
    if (!this.events[eventType]) {
      this.events[eventType] = [];
    }
    this.events[eventType].push(listener);
  }
  fire(eventType, eventProperties) {
    if (this.events[eventType]) {
      this.events[eventType].forEach(listener => listener(eventProperties));
    }
  }
}
addEventListener程序员使用它来注册他们想要的listener函数,以便在执行他们想要的eventType.
请注意,对于每个 distinct eventType,都有一个不同的数组。该数组可以listener为同一个eventType.
fire由浏览器调用以响应用户交互。浏览器知道进行了何种交互以及在哪个节点上进行了交互。它使用该知识fire在适当的节点上调用适当的参数,即eventType和eventProperties。
fire循环遍历与特定 eventType 关联的数组。遍历数组,它listener在传递eventProperties给数组时调用数组中的每个函数。
这就是listener仅使用特定 eventType 注册的函数被调用一次的fire方式。
下面是一个演示。在这个演示中有 3 个演员。程序员、浏览器和用户。
let button = document.getElementById("myButton"); // Done by the Programmer
let button = new EventEmitter("myButton"); // Done by the Browser somewhere in the background. 
button.addEventListener("click", () =>
  console.log("This is one of the listeners for the click event. But it DOES NOT need the event details.")
); // Done By the Programmer
button.addEventListener("click", e => {
  console.log(
    "This is another listener for the click event! However this DOES need the event details."
  );
  console.log(e);
}); // Done By the Programmer
//User clicks the button
button.fire("click", {
  type: "click",
  clientX: 47,
  clientY: 18,
  bubbles: true,
  manyOthers: "etc"
}); // Done By the Browser in the background
用户点击按钮后,浏览器调用fire按钮,将“click”作为 aneventType和持有eventProperties. 这会导致调用listener“click”下的所有注册函数eventType。
正如你所看到的,浏览器总是把eventProperties着火。作为程序员,您可能会也可能不会在您的listener函数中使用这些属性。
我发现对 stackoveflow 有帮助的一些答案:
使用 addEventListener 注册的事件存储在哪里?
Javascript 事件处理程序存储在哪里?