I have just come accross with an issue related to event listening in Vue directives. I have a component which holds following code inside:
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);
}
}
}
...
}
And this component is re-rendered everytime I change router path, I achieved this behaviour by assigning current route path to :key
prop so whenever path changes it gets re-rendered. But the propblem is though event listeners are not being removed/destroyed causing terrible performance issues. So how do I remove event listeners?
Calling bind
on a function creates a new function. The listeners aren't being removed because the function you're passing to removeEventListener
is not the same function you passed to addEventListener
.
Communicating between hooks in directives is not particularly easy. The official documentation recommends using the element's dataset
, though that seems clumsy in this case:
https://v2.vuejs.org/v2/guide/custom-directive.html#Directive-Hook-Arguments
You could just store the listeners on the element directly as properties so that they're available in the unbind
hook.
The code below takes a slightly different approach. It uses an array to hold all of the elements that are currently bound to the directive. The listener on window
is only ever registered once, no matter how many times the directive is used. If the directive isn't currently being used then that listener is removed:
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/vue@2.6.11/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>
However, all of this assumes that a directive is even the right way to go. If you can just do this at the component level then it may get a lot simpler because you have the component instance available to store things. Just remember that calling bind
creates a new function, so you'll need to keep a reference to that function somewhere so you can pass it to removeEventListener
.