在組件卸載時正確移除事件監聽,是避免內存泄漏的關鍵操作,核心原則是 “監聽與移除的回調函數必須是同一引用,且移除時機與組件生命周期匹配”。不同框架(如 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 元素,避免遺漏任何一處監聽。
通過這三步,可確保組件卸載時事件監聽被完全移除,從根源上避免因監聽殘留導致的內存泄漏。 |