简体   繁体   English

无论如何定义回调,如何删除事件侦听器

[英]How can I remove an event listener no matter how the callback is defined

For years I ran into problems trying to remove an event listener in JavaScript. 多年来,我遇到了尝试在JavaScript中删除事件监听器的问题。 Often I would have to create an independent function as the handler. 通常,我不得不创建一个独立的函数作为处理程序。 But that is just sloppy and, especially with the addition of arrow functions, just a pain. 但这只是草率的,特别是在添加了箭头功能的情况下,这只是一种痛苦。

I am not after a ONCE solution. 我不寻求ONCE解决方案。 This needs to work in all situations no matter HOW the callback is defined. 无论如何定义回调,这都需要在所有情况下都有效。 And this needs to be raw JS so anyone can use it. 这必须是原始JS,以便任何人都可以使用它。

The following code works fine since the function clickHandler is a unique function and can be used by both addEventListener and removeEventListener : 由于函数clickHandler是一个唯一函数,因此以下代码可以正常工作,并且可以被addEventListenerremoveEventListener

This example has been updated to show what I have run into in the past 此示例已更新,以显示我过去遇到的问题

 const btnTest = document.getElementById('test'); let rel = null; function clickHandler() { console.info('Clicked on test'); } function add() { if (rel === null) { rel = btnTest.addEventListener('click', clickHandler); } } function remove() { btnTest.removeEventListener('click', clickHandler); } [...document.querySelectorAll('[cmd]')].forEach( el => { const cmd = el.getAttribute('cmd'); if (typeof window[cmd] === 'function') { el.addEventListener('click', window[cmd]); } } ); 
 <button cmd="add">Add</button> <button cmd="remove">Remove</button> <button id="test">Test</button> 

You used to be able to do it with arguments.callee : 您以前可以使用arguments.callee做到这一点:

 var el = document.querySelector('#myButton'); el.addEventListener('click', function () { console.log('clicked'); el.removeEventListener('click', arguments.callee); //<-- will not work }); 
 <button id="myButton">Click</button> 

But using an arrow function does not work: 但是使用箭头功能不起作用:

 var el = document.querySelector('#myButton'); el.addEventListener('click', () => { console.log('clicked'); el.removeEventListener('click', arguments.callee); //<-- will not work }); 
 <button id="myButton">Click</button> 

Is there a better way?? 有没有更好的办法??

UPDATE 更新

As stated by @Jonas Wilms this way will work: 如@Jonas Wilms所说,这种方式可以工作:

  var el = document.querySelector('#myButton'); el.addEventListener('click', function handler() { console.log('clicked'); el.removeEventListener('click', handler); //<-- will work }); 
 <button id="myButton">Click</button> 

Unless you need to using binding: 除非您需要使用绑定:

 var obj = { setup() { var el = document.querySelector('#myButton'); el.addEventListener('click', (function handler() { console.log('clicked', Object.keys(this)); el.removeEventListener('click', handler); //<-- will work }).bind(this)); } } obj.setup(); 
 <button id="myButton">Click</button> 

The problem is that there are too many ways to provide an event handler to the addEventListener function and your code might break if the way you pass in the function changes in a refactor. 问题在于,有太多方法可以为addEventListener函数提供事件处理程序,并且如果您在函数中传递的方式在重构中发生更改,则代码可能会中断。

You can NOT use an arrow function or any anonymous function directly and expect to be able to remove the listener. 不能直接使用箭头函数或任何匿名函数,并且期望能够删除侦听器。

To remove a listener requires you pass the EXACT SAME ARGUMENTS to removeEventListener as you passed to addEventListener but when you use an anonymous function or an arrow function you do not have access to that function so it's impossible for you to pass it into removeEventListener 要删除侦听removeEventListener ,您需要将传递给addEventListenerEXACT SAME ARGUMENTS传递给removeEventListener ,但是当您使用匿名函数或箭头函数时,您无权访问该函数,因此无法将其传递给removeEventListener

works 作品

const anonFunc = () => { console.log("hello"); }
someElem.addEventListener('click', anonFunc);    
someElem.removeEventListener('click', anonFunc);  // same arguments

does not work 不起作用

someElem.addEventListener('click', () => { console.log("hello"); });    
someElem.removeEventListener('click', ???) // you don't have a reference 
                                           // to the anon function so you
                                           // can't pass the correct arguments
                                           // to remove the listener

your choices are 您的选择是

  • don't use anonymous or arrow functions 不要使用匿名或箭头功能
  • use a wrappers that will track the arguments for you 使用包装程序将为您跟踪参数

One example is @Intervalia closure. 一个例子是@Intervalia闭包。 He tracks the function and other arguments you passed in and returns a function you can use the remove the listener. 他跟踪您传入的函数和其他参数,并返回可以使用remove侦听器使用的函数。

One solution I often use which often fits my needs is a class that tracks all the listeners and remove them all. 我经常使用的一种最适合我需要的解决方案是跟踪所有侦听器并将其全部删除的类。 Instead of a closure it returns an id but it also allows just removing all listeners which I find useful when I build up something now and want to tear it down something later 它不是返回闭包,而是返回一个id,但它也仅允许删除所有侦听器,这些侦听器在我现在构建某些东西并希望稍后将其拆除时对我有用。

 function ListenerManager() { let listeners = {}; let nextId = 1; // Returns an id for the listener. This is easier IMO than // the normal remove listener which requires the same arguments as addListener this.on = (elem, ...args) => { (elem.addEventListener || elem.on || elem.addListener).call(elem, ...args); const id = nextId++; listeners[id] = { elem: elem, args: args, }; if (args.length < 2) { throw new Error('too few args'); } return id; }; this.remove = (id) => { const listener = listeners[id]; if (listener) { delete listener[id]; const elem = listener.elem; (elem.removeEventListener || elem.removeListener).call(elem, ...listener.args); } }; this.removeAll = () => { const old = listeners; listeners = {}; Object.keys(old).forEach((id) => { const listener = old[id]; if (listener.args < 2) { throw new Error('too few args'); } const elem = listener.elem; (elem.removeEventListener || elem.removeListener).call(elem, ...listener.args); }); }; } 

Usage would be something like 用法就像

const lm = new ListenerManager();
lm.on(saveElem, 'click', handleSave);
lm.on(newElem, 'click', handleNew);
lm.on(plusElem, 'ciick', handlePlusOne);
const id = lm.on(rangeElem, 'input', handleRangeChange);

lm.remove(id);  // remove the input event on rangeElem

lm.removeAll();  // remove events on all elements managed by this ListenerManager

note the code above is ES6 and would have to be changed to support really old browsers but the ideas are the same. 请注意,上面的代码是ES6,必须进行更改以支持真正的旧浏览器,但是想法是相同的。

Just use a named function expression: 只需使用一个命名函数表达式:

 var el = document.querySelector('#myButton');

 el.addEventListener('click', function handler() {
   console.log('clicked');
   el.removeEventListener('click', handler); //<-- will work
 });

For sure that can be wrapped in a function: 确保可以将其包装在函数中:

  function once(selector, evt, callback) {
    var el = document.querySelector(selector);

    el.addEventListener(evt, function handler() {
      callback();
      el.removeEventListener(evt, handler); //<-- will work
   });
}

once("#myButton", "clicl", () => {
  // do stuff
 });

There is an easy solution using closures. 有一个使用闭包的简单解决方案。

By moving the code to both addEventListener and removeEventListener into a single function you can accomplish the task easily: 通过将代码同时移至addEventListenerremoveEventListener到一个函数中,可以轻松完成任务:

 function ael(el, evt, cb, options) { console.log('Adding', evt, 'event listener for', el.outerHTML); el.addEventListener(evt, cb, options); return function() { console.log('Removing', evt, 'event listener for', el.outerHTML); el.removeEventListener(evt, cb, options); } } const btnTest = document.getElementById('test'); let rel = null; function add() { if (rel === null) { rel = ael(btnTest, 'click', () => { console.info('Clicked on test'); }); } } function remove() { if (typeof rel === 'function') { rel(); rel = null; } } function removeAll() { rels.forEach(rel => rel()); } const rels = [...document.querySelectorAll('[cmd]')].reduce( (rels, el) => { const cmd = el.getAttribute('cmd'); if (typeof window[cmd] === 'function') { rels.push(ael(el, 'click', window[cmd])); } return rels; }, [] ); 
  <button cmd="add">Add</button> <button cmd="remove">Remove</button> <button id="test">Test</button> <hr/> <button cmd="removeAll">Remove All</button> 

The function ael above allows the element, the event type and the callback to all be saved in the closure scope of the function. 上面的函数ael允许元素,事件类型和回调都保存在函数的关闭范围内。 When you call ael it calls addEventListener and then returns a function that will call removeEventListener . 当您调用ael它将调用addEventListener ,然后返回一个将调用removeEventListener的函数。 Later in your code you call that returned function and it will successfully remove the event listener without worrying about how the callback function was created. 稍后在代码中,您将调用该返回的函数,它将成功删除事件侦听器,而不必担心如何创建回调函数。

Here is an es6 version: 这是es6版本:

const ael6 = (el, evt, cb, options) => (el.addEventListener(evt, cb, options), () => el.removeEventListener(evt, cb, options));

You can use the once option of EventTarget.addEventListener() : 您可以使用EventTarget.addEventListener()once选项

Note: supported by all browsers but IE. 注意:除IE外,所有浏览器均支持。

 var el = document.querySelector('#myButton'); el.addEventListener('click', () => { console.log('clicked'); }, { once: true }); 
 <button id="myButton">Click</button> 

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM