在 Vue 中正确移除事件监听的核心是 “在组件生命周期的合适时机,用相同的回调函数引用移除监听”,需结合 Vue 的生命周期钩子(如onUnmounted、beforeDestroy)和函数引用稳定性来实现,避免因监听残留导致内存泄漏。以下是针对 Vue 3 和 Vue 2 的具体实现方法及避坑指南:
Vue 3 的组合式 API 通过setup函数或<script setup>组织代码,需在组件挂载时添加监听,在组件卸载前(onUnmounted钩子)移除,同时确保回调函数引用唯一。
<template>
<div>监听窗口大小变化</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
// 1. 定义具名回调函数(确保整个组件生命周期内引用唯一)
function handleResize() {
console.log('窗口大小变化:', window.innerWidth);
}
// 2. 组件挂载时添加事件监听
onMounted(() => {
window.addEventListener('resize', handleResize);
});
// 3. 组件卸载时移除监听(关键步骤)
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
若监听的是组件内的 DOM 元素(如自定义按钮、列表),需先通过ref获取 DOM 引用,再添加 / 移除监听:
<template>
<button ref="myButton">点击按钮</button>
</template>
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
// 获取DOM引用
const myButton = ref(null);
// 定义回调函数
function handleClick() {
console.log('按钮被点击');
}
onMounted(() => {
// 确保DOM已挂载(myButton.value存在)
myButton.value.addEventListener('click', handleClick);
});
onUnmounted(() => {
// 组件卸载前移除监听
myButton.value.removeEventListener('click', handleClick);
});
</script>
若回调函数依赖 Vue 的响应式数据(如ref/reactive),需确保函数引用稳定,避免因数据更新导致函数重新创建(可配合useCallback缓存):
<template>
<div>计数:{{ count }}</div>
</template>
<script setup>
import { onMounted, onUnmounted, ref, useCallback } from 'vue';
const count = ref(0);
// 用useCallback缓存回调函数,依赖变化时才重新创建
const handleScroll = useCallback(() => {
// 依赖响应式数据count
console.log('滚动时计数:', count.value);
}, [count]); // 当count变化时,函数重新创建
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
</script>
Vue 2 的选项式 API 通过mounted添加监听,在beforeDestroy(或destroyed)钩子中移除,回调函数通常定义在methods中以保证引用稳定。
<template>
<div>监听滚动事件</div>
</template>
<script>
export default {
methods: {
// 回调函数定义在methods中,引用唯一
handleScroll() {
console.log('页面滚动了');
}
},
mounted() {
// 组件挂载后添加监听
window.addEventListener('scroll', this.handleScroll);
},
beforeDestroy() {
// 组件销毁前移除监听(关键)
window.removeEventListener('scroll', this.handleScroll);
}
};
</script>
通过$refs获取 DOM 元素,在mounted中添加监听,beforeDestroy中移除:
<template>
<div ref="content">内容区域</div>
</template>
<script>
export default {
methods: {
handleClick(e) {
console.log('内容区域被点击', e.target);
}
},
mounted() {
// 确保DOM已加载(this.$refs.content存在)
this.$refs.content.addEventListener('click', this.handleClick);
},
beforeDestroy() {
this.$refs.content.removeEventListener('click', this.handleClick);
}
};
</script>
- 错误示例:使用匿名函数或在生命周期内动态创建函数,导致
removeEventListener无法匹配引用:
onMounted(() => {
window.addEventListener('scroll', () => { console.log('滚动'); });
});
onUnmounted(() => {
window.addEventListener('scroll', () => { console.log('滚动'); });
});
- 解决方案:始终使用具名函数(如
function handleXXX())或通过useCallback(Vue 3)/methods(Vue 2)确保引用稳定。
- 错误示例:
addEventListener的第三个参数(useCapture)为true时,移除时未传入相同参数,导致移除失败:
window.addEventListener('click', handleClick, true);
window.removeEventListener('click', handleClick);
- 解决方案:移除时严格保持
useCapture参数与添加时一致:
window.removeEventListener('click', handleClick, true);
- 问题:若组件内 DOM 通过
v-if销毁,可能导致removeEventListener时 DOM 已不存在(如myButton.value为null)。
- 解决方案:移除前先判断 DOM 是否存在:
onUnmounted(() => {
if (myButton.value) {
myButton.value.removeEventListener('click', handleClick);
}
});
- 问题:使用第三方库(如 ECharts、地图库)时,若库内部绑定了事件,需按其文档调用销毁方法。
- 解决方案:在组件卸载时调用库的销毁函数,避免内部监听残留:
<script setup>
import { onMounted, onUnmounted, ref } from 'vue';
import * as echarts from 'echarts';
const chartRef = ref(null);
let chartInstance = null;
onMounted(() => {
chartInstance = echarts.init(chartRef.value);
// 第三方库可能内部绑定了事件
chartInstance.on('click', (params) => { /* 处理点击 */ });
});
onUnmounted(() => {
// 调用库的销毁方法,内部会自动移除事件监听
if (chartInstance) {
chartInstance.dispose();
}
});
</script>
- 定义稳定回调:用具名函数、
methods(Vue 2)或useCallback(Vue 3)确保回调函数引用唯一。
- 匹配生命周期:在
mounted(Vue 2)/onMounted(Vue 3)中添加监听,在beforeDestroy(Vue 2)/onUnmounted(Vue 3)中移除。
- 检查参数与 DOM:确保
removeEventListener的参数(回调、useCapture)与添加时一致,且操作 DOM 前判断其是否存在。
遵循以上步骤,可彻底避免 Vue 组件中因事件监听未移除导致的内存泄漏问题。 |