前端的内存泄漏通常不是指内存突然爆炸,而是不再需要的对象仍然被引用,导致垃圾回收机制无法释放它们。在 Vue 组件中,最常见的泄漏来源就是定时器和全局事件监听——它们在组件销毁后依然存活,保持着对组件作用域内变量和 DOM 的引用,导致整个组件实例无法被回收。
定时器未清除
使用 setInterval 或 setTimeout 时,回调函数会捕获当前作用域中的变量(闭包)。如果组件卸载时定时器仍在运行,这些被捕获的变量(包括组件实例本身)就会一直占用内存。
反模式示例:
<script setup>
import { ref } from 'vue'
const count = ref(0)
// 组件挂载时开始计时
setInterval(() => {
count.value++
}, 1000)
// 组件卸载后,定时器依然在跑,count 和组件实例都无法被回收
</script>
正确做法:在组件卸载时清除定时器
<script setup>
import { ref, onUnmounted } from 'vue'
const count = ref(0)
const timer = setInterval(() => {
count.value++
}, 1000)
onUnmounted(() => {
clearInterval(timer)
})
</script>
如果是 setTimeout,同样需要在 onUnmounted 中用 clearTimeout 清理。这能确保组件离开页面后,回调不再执行,闭包引用被解除。
全局事件监听未移除
通过 window.addEventListener、document.addEventListener 或在某个全局 DOM 元素上绑定的事件,不会因为组件的销毁而自动解绑。事件监听器持有对组件内部函数的引用,进而间接持有组件实例,形成泄漏链。
反模式示例:
<script setup>
import { ref } from 'vue'
const width = ref(window.innerWidth)
// 监听窗口大小变化
window.addEventListener('resize', () => {
width.value = window.innerWidth
})
// 组件卸载后,resize 事件回调依然存在,width 和组件实例不会被回收
</script>
正确做法:在组件卸载时移除监听
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const width = ref(window.innerWidth)
const onResize = () => {
width.value = window.innerWidth
}
onMounted(() => {
window.addEventListener('resize', onResize)
})
onUnmounted(() => {
window.removeEventListener('resize', onResize)
})
</script>
第三方库或 DOM 手动绑定的事件
如果手动通过原生方法给某个 DOM 元素绑定了事件,或使用第三方库(如 Swiper、ECharts)初始化了实例,同样需要在组件销毁时手动销毁实例或解绑事件。通常可以利用 onUnmounted 或 watch 配合 cleanup 函数来处理。
实用建议:
- 养成成对出现的习惯:凡是有 “start” 或 “add” 的地方,就要有对应的 “stop” 或 “remove” 在
onUnmounted中执行。 - 使用工具函数或 Hooks 封装:将定时器、事件监听等副作用封装成可复用的组合式函数,内部自动处理清理逻辑,避免在每个组件里重复写清理代码。
- 使用
watch或watchEffect的onCleanup:当副作用依赖某些响应式数据时,可以在侦听器回调中注册清理函数,确保数据变化或组件卸载时清理前一次的副作用。
内存泄漏在中小型页面中可能不易察觉,但在单页应用中,用户频繁切换路由却不刷新页面,泄漏的累积会逐渐占用更多内存,最终导致页面卡顿甚至崩溃。“谁创建,谁销毁” 是防止这类问题的黄金法则。