移除使用bind添加的事件监听器

219
在JavaScript中,使用bind()添加的函数作为事件侦听器,最好的方法是什么来删除它?
示例
(function(){

    // constructor
    MyClass = function() {
        this.myButton = document.getElementById("myButtonID");
        this.myButton.addEventListener("click", this.clickListener.bind(this));
    };

    MyClass.prototype.clickListener = function(event) {
        console.log(this); // must be MyClass
    };

    // public method
    MyClass.prototype.disableButton = function() {
        this.myButton.removeEventListener("click", ___________);
    };

})();
我能想到的唯一方法是跟踪每个使用bind添加的侦听器。
使用此方法的示例:
(function(){

    // constructor
    MyClass = function() {
        this.myButton = document.getElementById("myButtonID");
        this.clickListenerBind = this.clickListener.bind(this);
        this.myButton.addEventListener("click", this.clickListenerBind);
    };

    MyClass.prototype.clickListener = function(event) {
        console.log(this); // must be MyClass
    };

    // public method
    MyClass.prototype.disableButton = function() {
        this.myButton.removeEventListener("click", this.clickListenerBind);
    };

})();

有更好的方法可以做到这一点吗?


4
除了 this.clickListener = this.clickListener.bind(this);this.myButton.addEventListener("click", this.clickListener); 以外,您在做什么? - Esailija
这非常好。这可能是一个不同的话题,但它让我想知道是否应该为使用“this”关键字的其余方法做bind(this),即使这会使方法调用变得低效。 - takfuruya
无论我是否会在以后删除它们,对于所有将要传递到其他地方的方法,我总是首先在构造函数中执行此操作。但并非所有方法都需要这样做,只有那些需要传递的方法才需要。 - Esailija
@machineghost,非常感谢您的建议,正是我所需要的。我的架构相对复杂,为了删除事件监听器而创建新的绑定函数将需要进行相当大的重构。此外,我已经在使用 Lodash。再次感谢您。 - Andi
很高兴能帮助到你 :) - machineghost
显示剩余2条评论
10个回答

351
尽管 @machineghost 所说的是真的,即事件的添加和删除方式相同,但方程式中缺失的部分是这个:

.bind() 调用后会创建一个新的函数引用。

请参见Does bind() change the function reference? | How to set permanently?

因此,要添加或删除它,请将引用分配给一个变量:

var x = this.myListener.bind(this);
Toolbox.addListener(window, 'scroll', x);
Toolbox.removeListener(window, 'scroll', x);

这对我来说符合预期。


4
好的,这个答案应该被接受。感谢您更新一个旧话题,这个话题在搜索引擎中排名第一,直到你发布了这篇文章才有了一个适当的解决方案。 - Blargh
1
这与问题中提到的方法没有任何区别(也不比它更好)。 - Peter Tseng
1
现代浏览器使用.addEventListener(type, listener).removeEventListener(type, listener)在元素上添加和删除事件。对于两者,您可以将解决方案中描述的函数引用作为listener参数传递,并将类型设置为“click”。https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener - Ben
2
这虽然是4年前发布的答案,但对我仍有帮助 :) - user2609021
1
确保在调用后续的 addListener()removeListener() 之前,不要替换(更新)x 监听器。否则,您将失去对先前监听器的引用。 - Aryo
显示剩余7条评论

55

对于那些在将React组件注册/移除监听器到/从Flux store时遇到此问题的人,请将以下行添加到组件构造函数中:

class App extends React.Component {
  constructor(props){
    super(props);
    // it's a trick! needed in order to overcome the remove event listener
    this.onChange = this.onChange.bind(this);  
  }
  // then as regular...
  componentDidMount (){
    AppStore.addChangeListener(this.onChange);
  }
  
  componentWillUnmount (){
    AppStore.removeChangeListener(this.onChange);
  }

  onChange () {
    let state = AppStore.getState();
    this.setState(state);
  }
  
  render() {
    // ...
  }
  
}


10
不错的技巧,但React/Flux与此有何关系? - Peter Tseng
这似乎是在从不同的类或原型函数中添加和删除事件侦听器时的正确方法,我认为这也适用于React组件/类。您可以在公共(例如根)实例级别上绑定它。 - Keith DC
1
this.onChange = this.onChange.bind(this) 其实这就是我一直在寻找的。将函数绑定到 this 上,以后永远不会改变 :) - Paweł

2
无论您是否使用绑定函数,删除它的方式与删除任何其他事件处理程序的方式相同。 如果您的问题在于绑定版本是其自己独特的函数,则可以跟踪绑定版本,或者使用不带特定处理程序的removeEventListener签名(尽管当然会删除相同类型的其他事件处理程序)。
(顺便说一下,不是所有浏览器都支持addEventListener;您真的应该使用像jQuery这样的库以跨浏览器的方式为您执行事件连接。 此外,jQuery具有命名空间事件的概念,允许您绑定到“click.foo”; 当您想要删除事件时,可以告诉jQuery“删除所有foo事件”,而无需知道特定的处理程序或删除其他处理程序。)

我知道IE的问题。我正在开发一个严重依赖canvas的应用程序,所以IE7及以下版本不支持。IE8支持canvas,但是最低限度。IE9+支持addEventListener。jQuery的命名空间事件看起来非常整洁。我唯一担心的是效率。 - takfuruya
jQuery的开发人员非常努力地保持其库的高性能,所以我不会太担心这个问题。然而,考虑到您对浏览器的严格要求,您可能想尝试一下Zepto。它有点像是一个精简版的jQuery,速度更快,但无法支持旧版浏览器(并且有一些其他限制)。 - machineghost
JQuery 命名空间事件被广泛使用,几乎没有性能问题。告诉别人不要使用可以使他们的代码更容易(并且可以说更重要的是)更易于理解的工具,这将是可怕的建议,特别是如果出于对 JQuery 和想象中的性能问题的非理性恐惧而这样做。 - machineghost
1
那将是哪个签名?removeEventListener的MDN页面显示前两个参数都是必需的。 - Coderer
我的错误。我写下那个答案已经过去很多年了,但是我可能当时想的是 jQuery 的 off 或者 unbind 方法。要移除元素上的所有监听器,你必须跟踪它们被添加的情况(这是 jQuery 或其他库可以为你做的事情)。 - machineghost

1
jQuery 解决方案:
let object = new ClassName();
let $elem = $('selector');

$elem.on('click', $.proxy(object.method, object));

$elem.off('click', $.proxy(object.method, object));

1
我们遇到了一个无法更改的库的问题。Office Fabric UI,这意味着我们无法更改添加事件处理程序的方式。我们解决它的方法是覆盖EventTarget原型上的addEventListener
这将在对象上添加一个新函数element.removeAllEventListers("click")
(原始帖子:从fabric对话框叠加中删除单击处理程序
        <script>
            (function () {
                "use strict";

                var f = EventTarget.prototype.addEventListener;

                EventTarget.prototype.addEventListener = function (type, fn, capture) {
                    this.f = f;
                    this._eventHandlers = this._eventHandlers || {};
                    this._eventHandlers[type] = this._eventHandlers[type] || [];
                    this._eventHandlers[type].push([fn, capture]);
                    this.f(type, fn, capture);
                }

                EventTarget.prototype.removeAllEventListeners = function (type) {
                    this._eventHandlers = this._eventHandlers || {};
                    if (type in this._eventHandlers) {
                        var eventHandlers = this._eventHandlers[type];
                        for (var i = eventHandlers.length; i--;) {
                            var handler = eventHandlers[i];
                            this.removeEventListener(type, handler[0], handler[1]);
                        }
                    }
                }

                EventTarget.prototype.getAllEventListeners = function (type) {
                    this._eventHandlers = this._eventHandlers || {};
                    this._eventHandlers[type] = this._eventHandlers[type] || [];
                    return this._eventHandlers[type];
                }

            })();
        </script>

0

这里是解决方案:

var o = {
  list: [1, 2, 3, 4],
  add: function () {
    var b = document.getElementsByTagName('body')[0];
    b.addEventListener('click', this._onClick());

  },
  remove: function () {
    var b = document.getElementsByTagName('body')[0];
    b.removeEventListener('click', this._onClick());
  },
  _onClick: function () {
    this.clickFn = this.clickFn || this._showLog.bind(this);
    return this.clickFn;
  },
  _showLog: function (e) {
    console.log('click', this.list, e);
  }
};


// Example to test the solution
o.add();

setTimeout(function () {
  console.log('setTimeout');
  o.remove();
}, 5000);

0

正如其他人所说,bind会创建一个新的函数实例,因此除非以某种方式记录事件监听器,否则无法删除它。

为了更美观的代码风格,您可以将方法函数设置为惰性获取器,这样当第一次访问时,它会自动替换为绑定版本:

class MyClass {
  activate() {
    window.addEventListener('click', this.onClick);
  }

  deactivate() {
    window.removeEventListener('click', this.onClick);
  }

  get onClick() {
    const func = (event) => {
      console.log('click', event, this);
    };
    Object.defineProperty(this, 'onClick', {value: func});
    return func;
  }
}

如果不支持ES6箭头函数,请使用const func = (function(event){...}).bind(this)代替const func = (event) => {...}
Raichman Sergey的方法也很好,特别是对于类。这种方法的优点是更加自包含,没有其他分离的代码。它还适用于没有构造函数或初始化程序的对象。

-1
如果你想使用上面建议的 'onclick',你可以尝试这样做:
(function(){
    var singleton = {};

    singleton = new function() {
        this.myButton = document.getElementById("myButtonID");

        this.myButton.onclick = function() {
            singleton.clickListener();
        };
    }

    singleton.clickListener = function() {
        console.log(this); // I also know who I am
    };

    // public function
    singleton.disableButton = function() {
        this.myButton.onclick = "";
    };
})();

希望它有所帮助。


-1

可以使用关于ES7的内容:

class App extends React.Component {
  constructor(props){
    super(props);
  }
  componentDidMount (){
    AppStore.addChangeListener(this.onChange);
  }

  componentWillUnmount (){
    AppStore.removeChangeListener(this.onChange);
  }

  onChange = () => {
    let state = AppStore.getState();
    this.setState(state);
  }

  render() {
    // ...
  }

}

add/removeChangeListener 是 React 特有的方法,不属于 ES7。 - Neil VanLandingham

-2

虽然已经有一段时间了,但MDN对此有一个超级好的解释。那比这里的东西帮助我更多。

MDN :: EventTarget.addEventListener - The value of "this" within the handler

它提供了一个很好的handleEvent函数的替代方案。

这是一个使用bind和不使用bind的示例:

var Something = function(element) {
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // undefined, as this is the element
  };
  this.onclick2 = function(event) {
    console.log(this.name); // 'Something Good', as this is the binded Something object
  };
  element.addEventListener('click', this.onclick1, false);
  element.addEventListener('click', this.onclick2.bind(this), false); // Trick
}

上面示例中的一个问题是您无法使用bind删除侦听器。另一种解决方案是使用一个名为handleEvent的特殊函数来捕获任何事件:

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