阅读视图

发现新文章,点击刷新页面。

vue在页面退出前别忘记做好这些清理工作

最近在开发中遇到一个典型问题:页面需要通过轮询接口更新数据,但测试时发现网络请求异常频繁,甚至在页面切换后仍有旧请求持续触发。排查后发现,虽然在 onBeforeUnmount 中清除了定时器,但未取消已发出的接口请求 —— 这些请求完成后仍会执行回调逻辑,重新启动轮询,导致 “幽灵请求” 不断产生。

这个问题的根源在于:页面退出时的清理操作不彻底。本文将系统梳理页面卸载前必须执行的清理工作,以避免内存泄漏和不必要的资源消耗。

一、清除定时器/计时器

若组件中使用了 setTimeoutsetInterval,即使组件已卸载,未清除的定时器仍会占用内存并持续执行。

<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 绑定的全局事件(如 scrollresizekeydown)必须手动解绑,否则会持续触发。

<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页面退出或者组件销毁时清理需遵循 “谁创建,谁销毁;谁监听,谁解绑”的原则 ,主要包括以下场景:

  1. 清除定时器/计时器;
  2. 取消未完成的请求、关闭连接(WebSocket);
  3. 解绑全局事件、自定义事件;
  4. 销毁第三方插件实例;
❌