vue在页面退出前别忘记做好这些清理工作
最近在开发中遇到一个典型问题:页面需要通过轮询接口更新数据,但测试时发现网络请求异常频繁,甚至在页面切换后仍有旧请求持续触发。排查后发现,虽然在 onBeforeUnmount 中清除了定时器,但未取消已发出的接口请求 —— 这些请求完成后仍会执行回调逻辑,重新启动轮询,导致 “幽灵请求” 不断产生。
这个问题的根源在于:页面退出时的清理操作不彻底。本文将系统梳理页面卸载前必须执行的清理工作,以避免内存泄漏和不必要的资源消耗。
一、清除定时器/计时器
若组件中使用了 setTimeout、setInterval,即使组件已卸载,未清除的定时器仍会占用内存并持续执行。
<script setup>
let timer = null;
onMounted(() => {
// 启动定时器
timer = setInterval(() => {
console.log('执行定时任务');
}, 1000);
});
onBeforeUnmount(() => {
// 组件销毁时清除
clearInterval(timer);
});
</script>
二、取消未完成的网络请求
对于一般页面中的交互请求,接口返回都非常快,因此在退出页面时不需要特别对这些接口进行取消操作。 但是对于一些返回比较慢的接口,尤其是跟上面说的和定时器轮询配合调用的接口,则需要在页面退出的时候进行手动的取消。 这是因为Promise一旦新建它就会立即执行,无法中途取消。也就是说当一个异步请求发出的时候,就不能中途取消了,不像setTimeout可以随时通过clearTimeout清除掉计时器。这也是为什么当页面中有网络请求发出时,即使页面关闭,页面对象被销毁,接口仍然会继续执行,并且接口返回后的逻辑也会被执行。所以对于这种情况需要我们手动处理,页面销毁的时候进行接口的取消操作。
<script setup>
import axios from 'axios';
const CancelTokenSource = axios.CancelToken.source();
let timer = null;
const data = ref();
function startPollFunc(shouldPoll) {
const pollFunc = async () => {
const res = await axios.get('/api/polling-data', {
cancelToken: CancelTokenSource.token
});
data.value = res.data;
if (shouldPoll) {
timer = setTimeout(pollFunc, 5000);
}
}
// 初始调用
pollFunc();
}
onMounted(() => {
startPollFunc(true);
});
onBeforeUnmount(() => {
// 清除定时器
clearTimeout(timer);
// 取消接口
CancelTokenSource.cancel();
});
</script>
在页面退出时除了需要注意这种普通接口的取消,对于websocket这种网络请求,也不要忘记关闭。
<script setup>
let ws = null;
onMounted(() => {
ws = new WebSocket('wss://example.com/chat');
ws.onmessage = (e) => { /* 处理消息 */ };
});
onBeforeUnmount(() => {
if (ws) {
ws.close(1000, '页面卸载'); // 1000表示正常关闭
}
});
</script>
三、解绑全局事件监听
Vue 模板中通过 v-on 绑定的事件会随组件销毁自动解绑。但通过 window/document 绑定的全局事件(如 scroll、resize、keydown)必须手动解绑,否则会持续触发。
<script setup>
const handleScroll = () => {
console.log('页面滚动了');
};
onMounted(() => {
window.addEventListener('scroll', handleScroll);
});
onBeforeUnmount(() => {
// 解绑事件(必须使用同一个函数引用)
window.removeEventListener('scroll', handleScroll);
});
</script>
四、清理自定义事件总线
在 Vue 开发中,当组件间需要跨层级或非父子关系通信时,常会用到全局事件总线(如基于 mitt实现的 evtBus)。若使用了事件总线,需要在组件销毁时移除注册的事件,避免事件重复触发,引发逻辑错乱。这种情况与 “全局事件监听” 的清理逻辑本质一致。
<script setup>
import mitt from 'mitt'
const emitter = mitt();
const handleEvent = () => { /* 处理事件 */ };
onMounted(() => {
emitter.on('customEvent', handleEvent);
});
onBeforeUnmount(() => {
emitter.off('customEvent', handleEvent); // 移除事件
});
</script>
五、第三方插件对象的销毁
调用第三方插件(如Echarts、富文本编辑器)创建的对象,若不手动销毁,这些资源不会随组件卸载自动释放,导致内存占用持续增长。
<template>
<!-- 图表容器 -->
<div class="chart-container" ref="chartRef"></div>
</template>
<script setup>
import * as echarts from 'echarts'; // 引入 ECharts
// 图表容器 DOM 引用
const chartRef = ref(null);
// ECharts 实例对象
let chartInstance = null;
// 移除事件的句柄
let removeResizeListener;
// 初始化图表
const initChart = () => {
// 1. 创建 ECharts 实例(绑定到容器)
chartInstance = echarts.init(chartRef.value);
// 2. 配置图表选项(示例:折线图)
const option = {
......
};
// 3. 设置图表选项
chartInstance.setOption(option);
// 4. 监听窗口大小变化,自动调整图表(可选)
const handleResize = () => {
chartInstance.resize();
};
window.addEventListener('resize', handleResize);
// 返回清理函数(用于后续解绑事件)
return () => {
window.removeEventListener('resize', handleResize);
};
};
onMounted(() => {
if (chartRef.value) {
removeResizeListener = initChart();
}
});
// 组件即将卸载时销毁图表
onBeforeUnmount(() => {
// 1. 解绑窗口大小监听(避免内存泄漏)
if (removeResizeListener) {
removeResizeListener();
}
// 2. 销毁 ECharts 实例
if (chartInstance) {
chartInstance.dispose(); // 调用 ECharts 内置销毁方法
}
});
</script>
清理操作的最佳时机
vue在组件卸载的时候有两个生命周期,一个是onBeforeUnmount,一个是onUnmounted。那么上面说的清理操作在哪个生命周期中执行好呢?
我们来看下这两个生命周期的具体区别。
两者的核心区别在于执行时机。
-
onBeforeUnmount:组件即将被卸载,但DOM 仍未销毁,组件实例和数据依然可用。此时可以安全地访问组件内的变量(如定时器 ID、请求令牌),执行清理操作。 -
onUnmounted:组件已经被卸载,DOM 已销毁,组件实例开始被回收。虽然大部分情况下仍能访问变量,但存在潜在风险(如变量引用已被释放)。
通过对比两者的执行时机的区别,为了确保清理操作的可靠性,我会推荐在onBeforeUnmount生命周期阶段执行。在该阶段,轮询和接口请求依赖的组件内的变量引用绝对有效,清理操作能100%生效。
总结
vue页面退出或者组件销毁时清理需遵循 “谁创建,谁销毁;谁监听,谁解绑”的原则 ,主要包括以下场景:
- 清除定时器/计时器;
- 取消未完成的请求、关闭连接(WebSocket);
- 解绑全局事件、自定义事件;
- 销毁第三方插件实例;