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

时间:2018-11-12 16:10:40

标签: javascript ecmascript-6 es2017

多年来,我在尝试删除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
});
<button id="myButton">Click</button>

但是使用箭头功能不起作用:

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

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

有没有更好的办法?

更新

如@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>

除非,您需要使用绑定:

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>

问题在于为addEventListener函数提供事件处理程序的方法太多了,如果您在函数中传递的方式进行了重构,则代码可能会中断。

4 个答案:

答案 0 :(得分:2)

您可以直接使用箭头功能或任何匿名功能,并希望能够删除侦听器。

要删除监听器,您需要在传递给removeEventListener时将 EXACT SAME ARGUMENTS 传递给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,必须对其进行更改以支持真正的旧浏览器,但思路是相同的。

答案 1 :(得分:1)

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

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

答案 2 :(得分:0)

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

通过将代码同时移至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));

答案 3 :(得分:0)

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

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

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

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