Как я могу удалить прослушиватель событий независимо от того, как определен обратный вызов - PullRequest
0 голосов
/ 12 ноября 2018

В течение многих лет я сталкивался с проблемами при попытке удалить прослушиватель событий в JavaScript. Часто мне приходится создавать независимую функцию в качестве обработчика. Но это просто небрежно и, особенно с добавлением функций стрелок, просто боль.

Я не после разового решения. Это должно работать во всех ситуациях независимо от того, КАК определен обратный вызов. И это должен быть необработанный JS, чтобы любой мог его использовать.

Следующий код работает нормально, так как функция clickHandler является уникальной и может использоваться как addEventListener, так и removeEventListener:

Этот пример был обновлен, чтобы показать, с чем я столкнулся в прошлом

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>

Есть ли лучший способ ??

UPDATE

Как сказал @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 голосов
/ 12 ноября 2018

Вы можете НЕ напрямую использовать функцию стрелки или любую анонимную функцию и ожидать, что сможете удалить слушателя.

Для удаления слушателя необходимо передать ТОЧНО ЖЕ АРГУМЕНТОВ 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. Он отслеживает функцию и другие переданные вами аргументы и возвращает функцию, которую вы можете использовать при удалении слушателя.

Одним из решений, которое я часто использую и которое часто соответствует моим потребностям, является класс, который отслеживает всех слушателей и удаляет их всех. Вместо замыкания он возвращает идентификатор, но он также позволяет просто удалить всех слушателей, которые я нахожу полезными, когда я что-то собираю сейчас и хочу что-то разорвать позже

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 и должен быть изменен для поддержки действительно старых браузеров, но идеи те же.

0 голосов
/ 12 ноября 2018

Просто используйте выражение для именованной функции:

 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
 });
0 голосов
/ 12 ноября 2018

Вы можете использовать опцию once из EventTarget.addEventListener():

Примечание: поддерживается всеми браузерами, кроме IE.

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

el.addEventListener('click', () => {
  console.log('clicked');
}, { once: true });
<button id="myButton">Click</button>
0 голосов
/ 12 ноября 2018

Существует простое решение с использованием крышек.

Переместив код в addEventListener и removeEventListener в одну функцию, вы легко сможете выполнить задачу:

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