【问题标题】:How can I remove an event listener no matter how the callback is defined无论如何定义回调,如何删除事件侦听器
【发布时间】:2019-04-15 09:36:48
【问题描述】:

多年来,我在尝试删除 JavaScript 中的事件侦听器时遇到了问题。通常我必须创建一个独立的函数作为处理程序。但这只是草率,尤其是在添加箭头功能的情况下,只是一种痛苦。

我不追求 ONCE 解决方案。无论如何定义回调,这都需要在所有情况下工作。这需要是原始 JS,以便任何人都可以使用它。

以下代码运行良好,因为函数clickHandler 是一个独特的函数,addEventListenerremoveEventListener 都可以使用:

此示例已更新以显示我过去遇到的问题

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>

你以前可以用arguments.callee做到这一点:

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

el.addEventListener('click', function () {
  console.log('clicked');
  el.removeEventListener('click', arguments.callee); //<-- will not work
});
&lt;button id="myButton"&gt;Click&lt;/button&gt;

但是使用箭头函数不起作用:

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

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

有没有更好的办法??

更新

正如@Jonas Wilms 所说,这种方式可行:

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

 el.addEventListener('click', function handler() {
   console.log('clicked');
   el.removeEventListener('click', handler); //<-- will work
 });
&lt;button id="myButton"&gt;Click&lt;/button&gt;

除非你需要使用绑定:

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();
&lt;button id="myButton"&gt;Click&lt;/button&gt;

问题在于向addEventListener 函数提供事件处理程序的方法太多,如果在重构中传递函数的方式发生变化,您的代码可能会中断。

【问题讨论】:

  • 或者你只使用 jQuerys $(sth).once("click", ...) ...

标签: javascript ecmascript-6 ecmascript-2017


【解决方案1】:

您可以直接使用箭头函数或任何匿名函数并期望能够移除侦听器。

要删除侦听器,您需要将 EXACT SAME ARGUMENTS 传递给 removeEventListener,就像您传递给 addEventListener 一样,但是当您使用匿名函数或箭头函数时,您无权访问它函数,所以你不可能将它传递给removeEventListener

作品

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

没用

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

你的选择是

  • 不要使用匿名函数或箭头函数
  • 使用为您跟踪参数的包装器

一个例子是@Intervalia 闭包。他跟踪你传入的函数和其他参数,并返回一个你可以使用的函数来删除监听器。

我经常使用的一个经常满足我需求的解决方案是一个跟踪所有听众并将它们全部删除的类。它不是闭包而是返回一个 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);
    });
  };
}

用法类似于

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

请注意,上面的代码是 ES6,必须进行更改以支持真正旧的浏览器,但想法是相同的。

【讨论】:

    【解决方案2】:

    只需使用命名函数表达式:

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

    确定可以包装在一个函数中:

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

    【讨论】:

    • @intervalua 我的回答仍然成立,至少第一部分如此。
    【解决方案3】:

    您可以使用EventTarget.addEventListener()once option

    注意:除 IE 以外的所有浏览器都支持。

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

    【讨论】:

    • 您的示例另有说明。我有一种感觉,您正在尝试推销自己的答案。很抱歉浪费了您的时间:)
    【解决方案4】:

    有一个使用闭包的简单解决方案。

    通过将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>

    上面的函数ael允许元素、事件类型和回调都保存在函数的闭包范围内。当您调用ael 时,它会调用addEventListener,然后返回一个将调用removeEventListener 的函数。稍后在您的代码中调用返回的函数,它将成功删除事件侦听器,而无需担心回调函数是如何创建的。

    这里是一个 es6 版本:

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

    【讨论】:

      猜你喜欢
      • 2012-09-10
      • 2022-01-18
      • 2017-01-23
      • 1970-01-01
      • 1970-01-01
      • 2018-09-04
      • 1970-01-01
      • 2010-10-09
      相关资源
      最近更新 更多