删除 class 中定义的 EventListener

Remove EventListener defined in a class

我正在尝试删除一个 eventListener,但我似乎遗漏了什么。

为什么下面的代码不起作用,它没有从按钮中删除事件侦听器。

我也试过绑定这个来传递作用域,但是也没用

class Test {

  eventHandler(e) {
    console.log(e.target.id)
    alert()

    // no effect
    e.target.removeEventListener("click", this.eventHandler)

    // no effect either
    document.getElementById(e.target.id).removeEventListener("click", this.eventHandler)
  }
  constructor() {
    let b = document.getElementById("b")
    b.addEventListener("click", this.eventHandler)

    //b.addEventListener("click", this.eventHandler.bind(this) )
  }
}

new Test()
<button id="b">
click me
</button>

OP 的代码不起作用有两个原因。

  • 在一种情况下,原型 eventHandler 错过了正确的 this 上下文。
  • 对于 运行 this.eventHandler.bind(this) 的第二种情况,创建了一个新的(处理程序)函数,但没有保存对它的引用。因此 removeEventHandler 永远不会引用正确的事件处理程序。

可能的解决方案...

function handleTestClickEvent(evt) {

  console.log(evt.currentTarget);
  console.log(this);
  console.log(this.eventHandler);

  // remove the instance specific (`this` context) `eventHandler`.
  evt.currentTarget.removeEventListener('click', this.eventHandler);
}

class Test {
  constructor() {
    // create own eventHandler with bound `this` context.
    this.eventHandler = handleTestClickEvent.bind(this);

    document
      .querySelector('#b')
      .addEventListener('click', this.eventHandler);
  }
}
new Test();
<button id="b">click me</button>

另一种可能的方法是使用基于箭头函数的、因此特定于实例的事件处理程序。箭头函数不支持显式 this 绑定。他们总是参考他们实施的上下文。

class Test {
  constructor() {
    // arrow-function based, thus instance-specific event-handler.
    this.eventHandler = evt => {

      console.log(evt.currentTarget);
      console.log(this);

      evt.currentTarget.removeEventListener('click', this.eventHandler);
    }
    document
      .querySelector('#b')
      .addEventListener('click', this.eventHandler);
  }
}
new Test();
<button id="b">click me</button>

然而,这两种方法都表明,引用特定事件处理程序的原型实现不是应该遵循的路径。

对于 OP 提供的场景,我会选择第一个解决方案,因为它提供了本地实现的代码重用 handleTestClickEvent。它还带有关于实例特定 this.eventHandler 的较小足迹,前者是从 handleTestClickEvent.bind(this) 创建的,而第二个解决方案为每个实例提供完整的处理程序实现。

您的代码看起来太复杂了。如果你想禁用点击处理程序,有几种方法可以做到这一点而无需删除处理程序。 Event delegation 让您在这里的生活也更轻松。在代码段中,button#b 在点击 3 次后被禁用,使用数据属性。 button#a可以无限点击

document.addEventListener("click", handle);

function handle(evt) {
  if (evt.target.id === "b" && +(evt.target.dataset.nclicks || 1) < 3) {
    const nClicks = +(evt.target.dataset.nclicks || 0);
    evt.target.dataset.nclicks = nClicks + 1;

    if (nClicks === 2) {
      evt.target.setAttribute("disabled", "disabled");
    }
    
    return console.log(`you clicked button#b${
      nClicks === 2 ? ", no more clicks 4u" : ""}`);
  }
  if (evt.target.id === "a") {
    console.clear();
    return console.log(`you clicked button#a on ${new Date().toLocaleString()}`);
  }
}
<button id="a">click me a</button>
<button id="b">click me b</button>

作为事件处理程序的原型方法有点问题,特别是当您同时需要绑定到实例的 this 值和对实际事件处理程序函数的引用时。

默认情况下,事件队列在事件绑定到的元素的上下文中调用处理程序。更改上下文很容易,但这会让您创建一个新函数,然后将其用作事件处理程序,并且该函数不再是原型中的方法。

如果要保持紧凑的class结构,一种方法是将事件处理程序方法定义为实例自己的属性,它们根本无法被继承。最简单的方法是在构造函数中将方法定义为箭头函数。

class Test {
  constructor() {
    this.eventHandler = e => {
      console.log(e.target.id);
      e.target.removeEventListener("click", this.eventHandler);
    };
    let b = document.getElementById("b");
    b.addEventListener("click", this.eventHandler);
  }
}

new Test();
<button id="b">Click me!</button>

箭头函数保留对其定义的词法环境的引用,事件队列不能覆盖上下文。现在,处理函数中的 this 已正确绑定到实例,并且 this.eventHandler 引用附加到事件的函数。

在创建自己的 属性 时使用 bind 内存消耗稍少的选项,如下所示:

class Test {
  constructor() {
    this.eventHandler = this.eventHandler.bind(this);
    let b = document.getElementById("b");
    b.addEventListener("click", this.eventHandler);
  }
  eventHandler (e) {
    console.log(e.target.id);
    e.target.removeEventListener("click", this.eventHandler);
  }
}

这里bind创建一个新的函数对象,然后调用原型中的方法,方法的实际代码没有重复。如果您写道:

,则大致相似
this.eventHandler = e => Test.prototype.eventHandler.call(this, e);

值得注意的是,当用底层原型 属性 具有的相同名称定义自己的 属性 时,原型 属性 不会被覆盖,它仅在实例中被隐藏, class 的多个实例仍将按预期工作。

另一种选择是创建您自己的“事件模型”,它为所有事件创建一个包装函数(如上面最后一个代码示例),并存储对该函数的引用。包装器使用 call 调用实际处理程序,它可以将所需的 this 值绑定到事件处理程序。存储的函数引用用于删除事件。构建这样一个 model 并不是非常复杂,但它提供了一些关于 this 绑定和本机事件模型如何工作的知识。