在取消绑定 Vue 指令生命周期中移除 window 事件侦听器
Remove window event listener on unbind Vue directive lifecycle
我刚刚遇到一个与 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
道具来实现此行为,因此每当路径更改时它都会重新渲染。但问题是事件监听器并没有 removed/destroyed 导致可怕的性能问题。那么如何删除事件侦听器?
对函数调用 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/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>
但是,所有这些都假设指令是正确的方法。如果您可以在组件级别执行此操作,那么它可能会变得简单得多,因为您拥有可用于存储内容的组件实例。请记住,调用 bind
会创建一个新函数,因此您需要在某处保留对该函数的引用,以便将其传递给 removeEventListener
.
仅供记录,并帮助任何经过这里的人,因为已经有一个可接受的答案,在这种情况下(至少在 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);
}
}
}
...
}
在我的例子中,我正在做的一个实际例子是为任何输入或文本区域标签设置一个 focus/blur 通知程序。我做了一个 Gist here,它是在一个使用 TypeScript 构建在 Vue 3 上的项目上。
我刚刚遇到一个与 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
道具来实现此行为,因此每当路径更改时它都会重新渲染。但问题是事件监听器并没有 removed/destroyed 导致可怕的性能问题。那么如何删除事件侦听器?
对函数调用 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/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>
但是,所有这些都假设指令是正确的方法。如果您可以在组件级别执行此操作,那么它可能会变得简单得多,因为您拥有可用于存储内容的组件实例。请记住,调用 bind
会创建一个新函数,因此您需要在某处保留对该函数的引用,以便将其传递给 removeEventListener
.
仅供记录,并帮助任何经过这里的人,因为已经有一个可接受的答案,在这种情况下(至少在 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);
}
}
}
...
}
在我的例子中,我正在做的一个实际例子是为任何输入或文本区域标签设置一个 focus/blur 通知程序。我做了一个 Gist here,它是在一个使用 TypeScript 构建在 Vue 3 上的项目上。