在组件卸载时正确移除事件监听,是避免内存泄漏的关键操作,核心原则是 “监听与移除的回调函数必须是同一引用,且移除时机与组件生命周期匹配”。不同框架(如 Vue、React)和原生 JS 的实现方式略有差异,但底层逻辑一致,以下是具体落地方法:
事件监听的移除依赖 “回调函数引用一致”,若移除时的回调与监听时不同(如匿名函数、每次渲染重新创建的函数),则无法正确移除,导致内存泄漏。
错误示例(匿名函数无法移除):
window.addEventListener('scroll', () => { console.log('滚动了'); });
window.removeEventListener('scroll', () => { console.log('滚动了'); });
正确示例(使用具名函数):
function handleScroll() {
console.log('滚动了');
}
window.addEventListener('scroll', handleScroll);
window.removeEventListener('scroll', handleScroll);
框架组件有明确的生命周期,需在 “组件即将卸载” 的生命周期钩子中执行移除操作,确保与组件生命周期同步。
在onMounted中添加监听,在onUnmounted中移除,使用函数声明或在setup中定义的具名函数确保引用一致。
<template>
<div>监听滚动的组件</div>
</template>
<script setup>
import { onMounted, onUnmounted } from 'vue';
// 定义具名回调函数(在setup中声明,确保整个组件生命周期内引用唯一)
function handleScroll() {
console.log('页面滚动了');
}
// 组件挂载时添加监听
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
// 组件卸载时移除监听(关键步骤)
onUnmounted(() => {
window.removeEventListener('scroll', handleScroll);
});
</script>
在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>
在componentDidMount中添加监听,在componentWillUnmount中移除,回调函数用this绑定确保引用一致。
class ScrollComponent extends React.Component {
handleScroll = () => {
console.log('页面滚动了');
};
componentDidMount() {
window.addEventListener('scroll', this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener('scroll', this.handleScroll);
}
render() {
return <div>监听滚动的组件</div>;
}
}
在useEffect中添加监听,利用useEffect的返回函数(清理函数)移除监听,确保回调函数引用稳定(可配合useCallback避免函数重复创建)。
import { useEffect, useCallback } from 'react';
const ScrollComponent = () => {
const handleScroll = useCallback(() => {
console.log('页面滚动了');
}, []);
useEffect(() => {
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, [handleScroll]);
return <div>监听滚动的组件</div>;
};
在原生 JS 中,若存在类似 “组件” 的概念(如动态创建 / 销毁的弹窗、模块),需在 “销毁函数” 中主动移除监听。
const Popup = {
init() {
this.dom = document.createElement('div');
this.dom.textContent = '弹窗内容';
document.body.appendChild(this.dom);
this.dom.addEventListener('click', this.handleClose);
},
handleClose: function() {
Popup.destroy();
},
destroy() {
this.dom.removeEventListener('click', this.handleClose);
document.body.removeChild(this.dom);
this.dom = null;
}
};
Popup.init();
-
回调函数引用变化导致移除失败
- 问题:若回调函数在每次渲染时重新创建(如 React 中未用
useCallback的函数、Vue 中在onMounted内定义的函数),会导致removeEventListener找不到相同引用。
- 解决:用框架提供的缓存方法(如
useCallback、methods)确保函数引用稳定。
-
遗漏 “非 window/document” 的事件监听
- 问题:除了
window、document,对自定义 DOM 元素(如div、button)的监听也需移除,尤其动态创建的元素。
- 解决:在元素被移除前,先移除其上的所有监听(如 Vue 的
v-if销毁元素前,在onUnmounted中处理)。
-
事件监听的 “捕获 / 冒泡” 阶段不匹配
- 问题:
addEventListener的第三个参数(useCapture)若为true(捕获阶段),移除时也需传入true,否则无法匹配。
- 解决:移除时严格保持
useCapture参数与监听时一致:
window.addEventListener('click', handleClick, true);
window.removeEventListener('click', handleClick, true);
- 定义稳定引用的回调函数:用具名函数、框架缓存方法(
useCallback、methods)确保监听与移除时引用一致。
- 在组件卸载生命周期中移除:Vue 的
onUnmounted、React 的componentWillUnmount/useEffect清理函数、原生 JS 的destroy方法,是佳时机。
- 覆盖所有监听对象:包括
window、document、自定义 DOM 元素,避免遗漏任何一处监听。
通过这三步,可确保组件卸载时事件监听被完全移除,从根源上避免因监听残留导致的内存泄漏。 |