Удалить прослушиватель событий окна при жизненном цикле директивы unbind Vue

Я только что столкнулся с проблемой, связанной с прослушиванием событий в директивах Vue. У меня есть компонент, внутри которого содержится следующий код:

function setHeaderWrapperHeight() { ... }
function scrollEventHandler() { ... }

export default {
  ...
  directives: {
    fox: {
      inserted(el, binding, vnode) {
        setHeaderWrapperHeight(el);
        el.classList.add('header__unfixed');
        window.addEventListener(
          'scroll',
          scrollEventListener.bind(null, el, binding.arg)
        );
        window.addEventListener(
          'resize',
          setHeaderWrapperHeight.bind(null, el)
        );
      },
      unbind(el, binding) {
        console.log('Unbound');
        window.removeEventListener('scroll', scrollEventListener);
        window.removeEventListener('resize', setHeaderWrapperHeight);
      }
    }
  }
  ...
}

И этот компонент повторно отображается каждый раз, когда я меняю путь к маршрутизатору, я добился этого, назначив текущий путь маршрута для :key prop, поэтому при изменении пути он повторно отображается. Но проблема в том, что прослушиватели событий не удаляются / не уничтожаются, что вызывает ужасные проблемы с производительностью. Итак, как мне удалить слушателей событий?


person thisismamatto    schedule 10.02.2020    source источник
comment
Попробуйте удалить прослушиватели событий в ловушке жизненного цикла компонента уничтожено, а не внутри директивы ,   -  person DigitalDrifter    schedule 10.02.2020
comment
@DigitalDrifter По-прежнему получаю те же результаты   -  person thisismamatto    schedule 10.02.2020
comment
@DigitalDrifter Да, я использую VueRouter. Как выяснилось, проблема была более специфичной для javascript, поэтому при вызове bind() в функции была создана новая функция, и поэтому removeEventListener получал неправильную ссылку на функцию. Спасибо!   -  person thisismamatto    schedule 11.02.2020


Ответы (2)


Вызов bind функции создает новую функцию. Слушатели не удаляются, потому что функция, которую вы передаете removeEventListener, - это не та же функция, которую вы передали addEventListener.

Взаимодействие между хуками в директивах не очень-то просто. Официальная документация рекомендует использовать dataset элемента, хотя в данном случае это кажется неуклюжим:

https://vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments

Вы можете просто сохранить слушателей в элементе непосредственно как свойства, чтобы они были доступны в ловушке unbind.

В приведенном ниже коде используется несколько иной подход. Он использует массив для хранения всех элементов, которые в настоящее время привязаны к директиве. Слушатель на window регистрируется только один раз, независимо от того, сколько раз используется директива. Если директива в настоящее время не используется, этот слушатель удаляется:

let foxElements = []

function onClick () {
  console.log('click triggered')

  for (const entry of foxElements) {
    clickHandler(entry.el, entry.arg)
  }
}

function clickHandler (el, arg) {
  console.log('clicked', el, arg)
}

new Vue({
  el: '#app',
  
  data () {
    return {
      items: [0]
    }
  },

  directives: {
    fox: {
      inserted (el, binding) {
        console.log('inserted')
        
        if (foxElements.length === 0) {
          console.log('adding window listener')
          window.addEventListener('click', onClick)
        }

        foxElements.push({
          el,
          arg: binding.arg
        })
      },

      unbind (el, binding) {
        console.log('unbind')
      
        foxElements = foxElements.filter(element => element.el !== el)
        
        if (foxElements.length === 0) {
          console.log('removing window listener')
          window.removeEventListener('click', onClick)
        }
      }
    }
  }
})
<script src="https://unpkg.com/[email protected]/dist/vue.js"></script>

<div id="app">
  <button @click="items.push(Math.floor(Math.random() * 1000))">Add</button>
  <hr>
  <button
    v-for="(item, index) in items"
    v-fox:example
    @click="items.splice(index, 1)"
  >Remove {{ item }}</button>
</div>

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

person skirtle    schedule 10.02.2020

Просто для записей и чтобы помочь тем, кто проходит здесь, поскольку уже есть принятый ответ, что можно сделать в этом случае (по крайней мере, на Vue 3, не тестировалось на Vue 2), так это использовать binding.dir (который является ссылкой на собственный объект директивы), чтобы разместить функцию добавления прослушивателя событий в объект директивы и вернуть ее позже, когда возникнет необходимость удалить этот прослушиватель.

Один простой пример (не связанный с исходным вопросом) для привязки одного события фокуса:

export default {
  ...
  directives: {
    fox: {
      handleFocus: () => { /* a placeholder to rewrite later */ },
      mounted(el, binding) {
        binding.dir.handleFocus = () => { /* do whatever */ }
        el.addEventListener('focus', binding.dir.handleFocus);
      },
      beforeUnmount(el, binding) {
        el.removeEventListener('focus', binding.dir.handleFocus);
      }
    }
  }
  ...
}

Практический пример того, что я делаю с этим, в моем случае - иметь уведомление о фокусе / размытии для любого тега input или textarea. Я сделал это здесь, в проекте, построенном на Vue 3 с TypeScript.

person Marcos Sandrini    schedule 14.05.2021