Eliminando el detector de eventos que se agregó con bind

164

En JavaScript, ¿cuál es la mejor manera de eliminar una función agregada como detector de eventos mediante bind ()?

Ejemplo

(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", ___________);
    };

})();

La única forma en que puedo pensar es hacer un seguimiento de cada oyente agregado con bind.

Ejemplo anterior con este método:

(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);
    };

})();

¿Hay alguna forma mejor de hacer esto?

takfuruya
fuente
2
Lo que estás haciendo excepto this.clickListener = this.clickListener.bind(this);ythis.myButton.addEventListener("click", this.clickListener);
Esailija
Eso es muy bueno. Este puede ser un tema diferente, pero me hizo preguntarme si debería vincular (this) para el resto de mis métodos que usan la palabra clave "this" aunque haría que las llamadas a métodos fueran ineficientes.
takfuruya
Siempre hago esto como primera cosa en el constructor para todos los métodos que se van a pasar a algún lado, independientemente de si los voy a eliminar más adelante. Pero no para todos los métodos, solo aquellos que se pasan.
Esailija
Lo que estás haciendo tiene sentido. Pero si esto formaba parte de una biblioteca, por ejemplo, nunca se puede saber qué métodos de MyClass (documentados como "públicos") se pasarían.
takfuruya
Solo para su información, la biblioteca Underscore tiene una bindAllfunción que simplifica los métodos de enlace. Dentro de su inicializador de objetos solo _.bindAll(this)debe configurar cada método de su objeto en una versión enlazada. Como alternativa, si sólo desea enlazar algunos métodos (que recomiendo, para evitar fugas accidentales de memoria), se les puede proporcionar como argumentos: _.bindAll(this, "foo", "bar") // this.baz won't be bound.
machineghost

Respuestas:

274

Aunque lo que @machineghost dijo era cierto, que los eventos se agregan y eliminan de la misma manera, la parte faltante de la ecuación fue esta:

¡Se crea una nueva referencia de función después de que .bind()se llama!

Ver ¿Bind () cambia la referencia de función? El | ¿Cómo configurar permanentemente?

Entonces, para agregarlo o eliminarlo, asigne la referencia a una variable:

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

Esto funciona como se esperaba para mí.

Ben
fuente
44
Excelente, esta debería ser la respuesta aceptada. Gracias por actualizar un tema antiguo, este tema apareció en el motor de búsqueda como el número uno y no tenía una solución adecuada hasta que lo publicaste ahora.
Blargh
Esto no difiere (y no es mejor) del método mencionado en la pregunta.
Peter Tseng
No puedo entender, ¿cómo hacer que funcione con un evento de clic, gracias
Alberto Acuña
@ AlbertoAcuña Los navegadores modernos usan .addEventListener(type, listener)y .removeEventListener(type, listener)para agregar y eliminar eventos en un elemento. Para ambos, puede pasar la referencia de función descrita en la solución como listenerparámetro, con "click"como tipo. developer.mozilla.org/en-US/docs/Web/API/EventTarget/…
Ben
1
esto me ayuda a pesar de que esta respuesta publicada hace 4 años :)
user2609021
46

Para aquellos que tienen este problema al registrar / eliminar el oyente del componente React a / desde la tienda Flux, agregue las líneas a continuación al constructor de su componente:

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() {
    // ...
  }
  
}

Raichman Sergey
fuente
77
Buen truco, pero ¿qué tiene que ver React / Flux con algo?
Peter Tseng
Este parece ser el enfoque correcto al agregar y eliminar oyentes de eventos de diferentes clases o funciones prototipo, que creo que la conexión con esto también se aplica a los componentes / clases React. Lo está vinculando a un nivel de instancia común (por ejemplo, raíz).
Keith DC
1
this.onChange = this.onChange.bind(this)En realidad esto es lo que estaba buscando. La función atada thispara siempre :)
Paweł
2

No importa si usa una función enlazada o no; lo elimina de la misma manera que cualquier otro controlador de eventos. Si su problema es que la versión enlazada es su propia función única, puede realizar un seguimiento de las versiones enlazadas o utilizar elremoveEventListener firma que no tiene un controlador específico (aunque, por supuesto, eliminará otros controladores de eventos del mismo tipo )

(Como nota al margen, addEventListenerno funciona en todos los navegadores; realmente debería usar una biblioteca como jQuery para hacer sus conexiones de eventos en un navegador cruzado para usted. Además, jQuery tiene el concepto de eventos con espacios de nombres, que permiten se debe vincular a "click.foo"; cuando desee eliminar el evento, puede decirle a jQuery "eliminar todos los eventos foo" sin tener que conocer el controlador específico o eliminar otros controladores).

maquina
fuente
Soy consciente del problema de IE. Estoy desarrollando una aplicación que depende en gran medida del lienzo, por lo que IE7- está fuera. IE8 admite lienzo pero como mínimo. IE9 + admite addEventListener. Los eventos de espacio de nombres de jQuery se ven muy bien. Lo único que me preocupa es la eficiencia.
takfuruya
La gente de jQuery trabaja muy duro para mantener su biblioteca funcionando bien, por lo que no me preocuparía demasiado por eso. Sin embargo, dados los estrictos requisitos de su navegador, puede consultar Zepto en su lugar. Es algo así como una versión reducida de jQuery que es más rápida pero no admite navegadores antiguos (y tiene algunos otros límites).
machineghost
Los eventos de espacio de nombres JQuery se usan ampliamente y prácticamente no tienen problemas de rendimiento. Decirle a alguien que no use una herramienta que haga que su código sea más fácil y (posiblemente más importante) más fácil de entender, sería un consejo horrible, especialmente si se hace por temor irracional a JQuery y preocupaciones de rendimiento imaginario.
machineghost
1
¿Qué firma sería esa? La página MDN en removeEventListener muestra que se requieren los dos primeros argumentos.
Coderer el
Mi error. Han pasado años desde que escribí esa respuesta, pero debo haber estado pensando en jQuery offo en el unbindmétodo. Para eliminar todos los oyentes en un elemento, debe realizar un seguimiento de ellos a medida que se agregan (que es algo que jQuery u otras bibliotecas pueden hacer por usted).
machineghost
1

Solución jQuery:

let object = new ClassName();
let $elem = $('selector');

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

$elem.off('click', $.proxy(object.method, object));
Ed Kolosovsky
fuente
1

Tuvimos este problema con una biblioteca que no pudimos cambiar. Office Fabric UI, lo que significa que no pudimos cambiar la forma en que se agregaron los controladores de eventos. La forma en que lo resolvimos fue sobrescribir addEventListenerel EventTargetprototipo.

Esto agregará una nueva función en los objetos element.removeAllEventListers("click")

(publicación original: Eliminar el controlador de clic de la superposición de diálogo de la tela )

        <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>
Peter
fuente
0

Aquí está la solución:

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);
Nazar Vynnytskyi
fuente
0

puede usar sobre 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() {
    // ...
  }

}
chiic
fuente
-1

Si desea usar 'onclick', como se sugirió anteriormente, puede intentar esto:

(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 = "";
    };
})();

Espero que ayude.

Diogo Schneider
fuente
-2

Ha pasado un tiempo, pero MDN tiene una súper explicación sobre esto. Eso me ayudó más que las cosas aquí.

MDN :: EventTarget.addEventListener - El valor de "this" dentro del controlador

Da una gran alternativa a la función handleEvent.

Este es un ejemplo con y sin enlace:

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
}

Un problema en el ejemplo anterior es que no puede eliminar el oyente con bind. Otra solución es usar una función especial llamada handleEvent para capturar cualquier evento:

Noitidart
fuente