使用bind(this)添加的事件监听器如何移除?

6

我该如何在下面的构造器移除绑定到window的点击监听器?我需要它监听window,并且我需要在其中访问按钮实例。

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    window.addEventListener('click', this.clickHandler.bind(this));
  }
  
  clickHandler(e) {
    if (e.target === this) {
      this.textContent = `clicked ${++this.clickCount} times`;
      window.removeEventListener('click', this.clickHandler);
    }
  }
  
  disconnectedCallback() {
      window.removeEventListener('click', this.clickHandler);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>


你掌控 JavaScript 代码了吗? - guest271314
1
这个回答解决了你的问题吗?移除使用bind添加的事件监听器 - caramba
3个回答

15

你现在的实现无法做到这一点 - 每次调用 .bind 都会创建一个新的独立函数,只有当传递给 addEventListener 的回调函数与传递给 removeEventListener 的回调函数相同 (===) ,才能调用 removeEventListener 删除监听器(就像对于数组的 .includes 或集合的 .has 一样):

const fn = () => 'foo';
console.log(fn.bind(window) === fn.bind(window));

作为一种解决方法,您可以将绑定函数分配给实例的属性:

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    this.boundListener = this.clickHandler.bind(this);
    window.addEventListener('click', this.boundListener);
  }
  
  clickHandler(e) {
    this.textContent = `clicked ${++this.clickCount} times`;
    window.removeEventListener('click', this.boundListener);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>


这基本上意味着您不能将在原型上定义的函数用作外部调用的事件侦听器/回调函数? - connexo
在某种程度上,传递给 addEventListener 的处理函数只能是原型上的方法,如果该方法不引用 this。如果需要使用 this,则需要绑定或使用类字段,或者传递一个处理函数,该函数不在原型上,例如在构造函数中定义的函数,其中 this 被正确设置。 - CertainPerformance

5
另一种模式是将您的监听器放在构造函数内部。
要删除事件侦听器(无论什么模式),您可以在创建事件侦听器时添加一个“remove”函数。
由于 remove 函数在 listen 作用域内调用,它使用相同的名称和功能。
伪代码:
  listen(name , func){
    window.addEventListener(name, func);
    return () => window.removeEventListener( name , func );
  }

  let remove = listen( 'click' , () => alert('BOO!') );

  //cleanup:
  remove();

运行以下“代码段”,以查看它如何与多个按钮一起使用。

事件冒泡和影子DOM

为了节省你在处理事件时的时间...

请注意,Web组件(即具有影子DOM的自定义元素)需要具有composed:true属性的CustomEvents,如果您想要它们冒泡超过其影子DOM边界。

    new CustomEvent("check", {
      bubbles: true,
      //cancelable: false,
      composed: true       // required to break out of shadowDOM
    });

移除已添加的事件监听器

注意:此示例在Safari上无法运行,因为苹果拒绝实现扩展元素:extends HTMLButtonElement

class MyEl extends HTMLButtonElement {
  constructor() {
    let ME = super();// super() retuns this scope; ME makes code easier to read
    let count = 0;// you do not have to stick everything on the Element
    ME.mute = ME.listen('click' , event => {
      //this function is in constructor scope, so has access to ALL its contents
      if(event.target === ME) //because ALL click events will fire!
        ME.textContent = `clicked ${ME.id} ${++count} times`;
      //if you only want to allow N clicks per button you call ME.mute() here
    });
  }

  listen(name , func){
    window.addEventListener( name , func );
    console.log('added' , name , this.id );
    return () => { // return a Function!
      console.log( 'removeEventListener' , name , 'from' , this.id);
      this.style.opacity=.5;
      window.removeEventListener( name , func );
    }
  }
  eol(){ // End of Life
    this.parentNode.removeChild(this);
  }
  disconnectedCallback() {
      console.log('disconnectedCallback');
      this.mute();
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
button{
  width:12em;
}
<button id="One" is="my-el" type="button">Click me</button>
<button onclick="One.mute()">Mute</button> 
<button onclick="One.eol()">Delete</button> 
<br>
<button id="Two" is="my-el" type="button">Click me too</button>
<button onclick="Two.disconnectedCallback()">Mute</button> 
<button onclick="Two.eol()">Delete</button> 

注意:

  • count 不是作为this.count 的形式出现,但它可以在构造函数内定义的所有函数中使用。所以它(有点)是私有的,只有 click 函数可以更新它。

  • onclick=Two.disconnectedCallback() 只是一个例子,该函数不会删除元素。


另请参见:https://pm.dartus.fr/blog/a-complete-guide-on-shadow-dom-and-event-propagation/


要么我没明白,要么你在添加监听器时漏掉了关键的.bind(this)部分(这才是我问题的真正原因)。 - connexo
“bind()”是关于设置作用域的,因为此示例中的监听器函数在“constructor”作用域中运行,所以无需使用“bind()”。我将更新第一句话以说“另一种模式”。 - Danny '365CSI' Engelman
如果您的clickhandler很大,而且有成千上万个按钮,内存是一个问题,那么您当然可以“bind”一个类函数。对于EventListener,相同的listen/mute方法适用►“ME.mute=Me.listen('click',ME.handler.bind(ME));”。 - Danny '365CSI' Engelman
不幸的是,你对this的理解在这里出了问题。该函数在监听器所附加到的元素/对象的作用域中运行,除非你使用.bind(this) - connexo
抱歉,我表达不够清楚。这里有两个不使用“bind()”的不同模式:https://jsfiddle.net/dannye/a6nvk173/ - Danny '365CSI' Engelman

4
创建一个包装器函数,用于处理您的 clickHandler,如下所示。

class MyEl extends HTMLButtonElement {
  constructor() {
    super();
    this.clickCount = 0;
    this.wrapper = e => this.clickHandler.apply(this, e);
    window.addEventListener('click', this.wrapper);
  }
  
  clickHandler(e) {
    this.textContent = `clicked ${++this.clickCount} times`;
    
    window.removeEventListener('click', this.wrapper);
  }
}

customElements.define('my-el', MyEl, { extends: 'button' });
<button is="my-el" type="button">Click me</button>


这是对我起作用的一个,但需要注意的是 apply 接受参数数组,而 call 则逐个接受,所以我最终使用了后者来传递事件。 - Carlos Garcia
在这种情况下,您甚至不需要使用apply,因为箭头函数会保留作用域。只需 e => this.clickHandler(e) - br4nnigan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接