Vue+el-admin管理后台,页面长时间运行,浏览器报:‘喔唷,崩溃啦! ’,定时器代码优化解决

前言

目录

前言

经排查发现

内存泄露问题分析

1. 缺少组件销毁时的清理逻辑

2. API请求没有取消机制

3. 全局对象引用 (这个看个人需求,因为我的页面还使用到了国际化语言,所以有这方面问题)

解决方法

1.创建一个Vue mixin来自动处理页面级别的请求取消。

requestCancel.js文件

request.js 文件 

使用方式:

2.优化了i18n引用:

思考

取消机制是否必要?

场景分析:

不取消请求的后果:

📊 分析结果

🚨 高风险场景:

  💡 具体问题示例(我的页面):

结论:取消机制非常必要!

为什么必要:

建议的实施策略:

优先级排序:

总结

📝 本次前端请求与内存优化总结

一、主要修改内容

1. 全局请求封装(request.js)

2. 页面级请求与定时器自动管理(requestCancel.js mixin)

3. CRUD 相关健壮性优化(crud.js)

4. 页面代码简化

二、适用场景

1. 所有有接口请求的页面

2. 有定时器的页面

3. 需要防止“接口请求失败”误报的场景

4. 所有 CRUD 相关页面

三、带来的好处

四、推荐用法

页面顶部引入 mixin:

 2.所有 API 调用用 safeApiCall 包裹:

3.定时器用 setSafeInterval、setSafeTimeout,不用手动清理。 

Vue 项目自动请求取消机制使用说明

一、核心文件

二、API 层改造(必须)

三、页面层用法

四、常见问题与注意事项

五、使用建议

七、效果


项目中 多个页面都有使用到定时器,调用频率在 每秒掉一次接口,最近长时间运行页面,会出现明显的卡顿 甚至浏览器崩溃,一直在排查,浏览器的PerformanceMemory监控内存,快照监控,各种小工具进行排查,也看到了网上很多 博文 说是:页面少用v-show/v-if,但我的页面排查下来 也很少用v-if,很明显不是这个问题。

经排查发现

我的页面很大一部分问题是在于 定时器的使用,还有就是页面数据量也比较大

内存泄露问题分析

1. 缺少组件销毁时的清理逻辑

示例:

在 消息 页面没有实现 beforeDestroy 或 destroyed 生命周期钩子来清理资源。虽然CRUD mixin会自动处理VM的注销,但页面本身可能还有其他需要清理的资源。

2. API请求没有取消机制

在 getStorageList() 方法中,API请求没有使用取消令牌,如果组件在请求完成前被销毁,可能会导致内存泄露。

3. 全局对象引用 (这个看个人需求,因为我的页面还使用到了国际化语言,所以有这方面问题)

在 logStatus 数据中使用了 window.vm.$i18n.t(),这种全局引用可能导致组件无法被正确垃圾回收。

解决方法

1.创建一个Vue mixin来自动处理页面级别的请求取消。

requestCancel.js文件

import {
  createPageCancelToken,
  cancelPageRequests,
  requestWithPageId,
} from "@/utils/request";

export default {
  data() {
    return {
      pageId: null,
      // 定时器管理
      timers: new Set(),
      // 请求管理
      pendingRequests: new Set(),
    };
  },

  created() {
    // 为每个页面实例生成唯一的页面ID
    this.pageId = `${this.$options.name || "page"}_${Date.now()}_${Math.random()
      .toString(36)
      .substr(2, 9)}`;

    // 创建页面级别的取消令牌
    createPageCancelToken(this.pageId);

    // 在全局设置当前页面的页面ID
    window.currentPageId = this.pageId;
  },

  beforeDestroy() {
    // 清理所有定时器
    this.clearAllTimers();

    // 取消所有pending请求
    this.cancelAllRequests();

    // 页面销毁时自动取消该页面的所有pending请求
    if (this.pageId) {
      cancelPageRequests(this.pageId);
    }

    // 清除全局页面ID
    if (window.currentPageId === this.pageId) {
      window.currentPageId = null;
    }
  },

  methods: {
    // 为API请求添加页面ID
    requestWithPageId(config) {
      return {
        ...config,
        pageId: this.pageId,
      };
    },

    // 安全地设置定时器(自动管理)
    setSafeInterval(callback, delay, ...args) {
      const timer = setInterval(callback, delay, ...args);
      this.timers.add(timer);
      return timer;
    },

    // 安全地设置延时器(自动管理)
    setSafeTimeout(callback, delay, ...args) {
      const timer = setTimeout(callback, delay, ...args);
      this.timers.add(timer);
      return timer;
    },

    // 清理特定定时器
    clearSafeTimer(timer) {
      if (timer) {
        clearInterval(timer);
        clearTimeout(timer);
        this.timers.delete(timer);
      }
    },

    // 清理所有定时器
    clearAllTimers() {
      this.timers.forEach((timer) => {
        clearInterval(timer);
        clearTimeout(timer);
      });
      this.timers.clear();
    },

    // 记录pending请求
    addPendingRequest(request) {
      this.pendingRequests.add(request);
      return request;
    },

    // 移除已完成的请求
    removePendingRequest(request) {
      this.pendingRequests.delete(request);
    },

    // 取消所有pending请求
    cancelAllRequests() {
      this.pendingRequests.forEach((request) => {
        if (request && typeof request.cancel === "function") {
          request.cancel("页面销毁,取消请求");
        }
      });
      this.pendingRequests.clear();
    },

    // 安全的API调用(自动添加页面ID和请求管理)
    async safeApiCall(apiFunction, ...args) {
      try {
        // 为API调用添加页面ID配置
        const config = { pageId: this.pageId };

        // 调用API函数,传递页面ID配置
        const result = await apiFunction(...args, config);
        return result;
      } catch (error) {
        // 如果是取消的请求,不抛出错误
        if (error.message && error.message.includes("取消")) {
          console.log("请求被取消:", error.message);
          return null;
        }
        throw error;
      }
    },
  },
};

request.js 文件 

import axios from "axios";
import router from "@/router/routers";
import { Notification } from "element-ui";
import store from "../store";
import { getToken } from "@/utils/auth";
import Config from "@/settings";
import Cookies from "js-cookie";
// import ipConfig from '../../public/config'

// 全局取消令牌源
let globalSource = axios.CancelToken.source();
// 存储各个页面的取消令牌
const pageCancelTokens = new Map();

// 创建axios实例
let service =
  service != null
    ? service
    : axios.create({
        baseURL: BASE_API,
        timeout: Config.timeout, // 请求超时时间
      });

// request拦截器
service.interceptors.request.use(
  (config) => {
    if (getToken()) {
      config.headers["Authorization"] = getToken(); // 让每个请求携带自定义token 请根据实际情况自行修改
    }
    var lang = localStorage.getItem("lang");
    if (!lang) {
      lang = "zh_CN";
    }
    config.headers["Accept-Language"] = lang.replace(/_/g, "-");
    config.headers["Content-Type"] = "application/json";

    // 优先使用页面级别的取消令牌,如果没有则使用全局的
    if (config.pageId && pageCancelTokens.has(config.pageId)) {
      config.cancelToken = pageCancelTokens.get(config.pageId).token;
    } else {
      config.cancelToken = globalSource.token;
    }

    config.cache = false;
    return config;
  },
  (error) => {
    return Promise.reject(error);
  }
);

// 取消所有请求(原有功能保持不变)
export function cancelAllReq() {
  // console.log('取消所有挂起状态的请求');
  globalSource.cancel("取消所有挂起状态的请求");

  // 取消所有页面级别的请求
  pageCancelTokens.forEach((source, pageId) => {
    source.cancel(`页面 ${pageId} 的请求被取消`);
  });
  pageCancelTokens.clear();

  // 重新创建全局取消令牌
  globalSource = axios.CancelToken.source();

  // 重新创建axios实例
  service = axios.create({
    baseURL: BASE_API,
    timeout: Config.timeout, // 请求超时时间
  });
}

// 新增:为特定页面创建取消令牌
export function createPageCancelToken(pageId) {
  const source = axios.CancelToken.source();
  pageCancelTokens.set(pageId, source);
  return source;
}

// 新增:取消特定页面的所有请求
export function cancelPageRequests(pageId) {
  if (pageCancelTokens.has(pageId)) {
    const source = pageCancelTokens.get(pageId);
    source.cancel(`页面 ${pageId} 的请求被取消`);
    pageCancelTokens.delete(pageId);
  }
}

// 新增:获取页面的取消令牌
export function getPageCancelToken(pageId) {
  return pageCancelTokens.get(pageId);
}

// response 拦截器
service.interceptors.response.use(
  (response) => {
    return response.data;
  },
  (error) => {
    // 新增:如果是取消请求,直接返回,不弹窗
    if (
      (typeof axios.isCancel === "function" && axios.isCancel(error)) ||
      (error.message && error.message.includes("取消"))
    ) {
      return Promise.reject(error);
    }
    // 兼容blob下载出错json提示
    if (
      error.response &&
      error.response.data instanceof Blob &&
      error.response.data.type &&
      error.response.data.type.toLowerCase().indexOf("json") !== -1
    ) {
      const reader = new FileReader();
      reader.readAsText(error.response.data, "utf-8");
      reader.onload = function (e) {
        const errorMsg = JSON.parse(reader.result).message;
        Notification.error({
          title: errorMsg,
          duration: 5000,
        });
      };
    } else {
      let code = 0;
      try {
        if (error.response && error.response.data) {
          code = error.response.data.status;
        }
      } catch (e) {
        if (error.toString().indexOf("Error: timeout") !== -1) {
          Notification.error({
            title: "网络请求超时",
            duration: 5000,
          });
          return Promise.reject(error);
        }
      }

      if (code) {
        if (code === 401) {
          store.dispatch("LogOut").then(() => {
            // 用户登录界面提示
            Cookies.set("point", 401);
            location.reload();
          });
        } else if (code === 403) {
          if (code === 403) {
            router.push({ path: "/401" });
          } else if (error.response && error.response.data) {
            const errorMsg = error.response.data.message;
            if (errorMsg !== undefined) {
              Notification.error({
                title: errorMsg,
                duration: 5000,
              });
            }
          }
        } else if (code === 400) {
          // 增加400错误提示
          if (error.response && error.response.data) {
            const errorMsg = error.response.data.message;
            if (errorMsg) {
              Notification.error({
                title: errorMsg,
                duration: 10000,
              });
            } else {
              Notification.error({
                title: "400 Error",
                duration: 10000,
              });
            }
          }
        }
      } else {
        Notification.error({
          title: "接口请求失败",
          duration: 5000,
        });
      }
    }
    return Promise.reject(error);
  }
);
export default service;

使用方式:

对于任何需要取消机制的页面,只需要:

import requestCancelMixin from "@/mixins/requestCancel";

export default {
  mixins: [requestCancelMixin],
  // ... 其他代码
}

然后使用:

  • this.safeApiCall(apiFunction, ...args) 替代直接调用API
  • this.setSafeInterval() 替代 setInterval()
  • this.setSafeTimeout() 替代 setTimeout()

取消机制现在完全透明,用户无感知,但能有效防止内存泄露!🎉

2.优化了i18n引用:

  • 将 window.vm.$i18n.t() 改为 this.$t()
  • 避免全局对象引用,提高垃圾回收效率

思考

取消机制是否必要?

场景分析:

  1. 快速切换页面 - 用户快速点击导航,页面A的请求还没完成就切换到页面B
  2. 组件频繁创建销毁 - 如弹窗、标签页等
  3. 搜索/筛选操作 - 用户快速输入,每次输入都触发请求
  4. 定时刷新 - 页面有定时器不断发送请求

不取消请求的后果:

  1. 内存泄露 - 未完成的Promise会一直占用内存
  2. 网络资源浪费 - 无用的请求继续占用带宽
  3. 数据竞争 - 后发的请求可能比先发的请求先返回
  4. 用户体验差 - 可能显示过期的数据

📊 分析结果

       在我的项目里确实非常需要取消机制,原因如下:

🚨 高风险场景:

  1. 大量定时器 - 项目中有50+个页面使用 setInterval,很多是1秒间隔的实时刷新
  2. 频繁搜索 - 大量页面有 @keyup.enter.native="crud.toQuery" 搜索功能
  3. 实时监控 - 多个看板页面有定时刷新机制

  💡 具体问题示例(我的页面):

// 这些页面都有定时器,如果不取消请求会导致严重的内存泄露
this.timer = setInterval(() => {
  setTimeout(async () => {
    this.getList()  // 每秒都在发送请求
  }, 0);
}, 1000);

结论:取消机制非常必要!

为什么必要:

  1. 防止内存泄露 - 定时器页面切换时,pending请求会一直占用内存
  1. 避免数据竞争 - 快速搜索时,后发的请求可能先返回
  1. 节省网络资源 - 取消无用的请求,减少服务器压力
  1. 提升用户体验 - 避免显示过期数据

建议的实施策略:

  1. 立即实施 - 对于有定时器的页面
  1. 逐步推广 - 对于搜索频繁的页面
  1. 全局配置 - 使用我们刚才创建的mixin

优先级排序:

  1. 高优先级 - 有定时器的看板页面
  1. 中优先级 - 有搜索功能的列表页面
  1. 低优先级 - 静态页面

总结

取消机制不是可选的,而是必需的!在我的项目中,有这么多实时刷新的页面,没有取消机制会导致严重的内存泄露和性能问题

📝 本次前端请求与内存优化总结

一、主要修改内容

1. 全局请求封装(request.js

  • 封装了 axios 实例,统一管理 baseURL、超时、token、语言等。
  • 全局 request/response 拦截器,自动处理 token、错误提示
  • 支持全局和页面级别的请求取消(CancelToken)。
  • 优化了错误处理,被取消的请求不会再弹出“接口请求失败”弹窗,用户体验更好。
  • 所有用到 error.response.data 的地方都加了健壮性判断,防止 undefined 报错。

2. 页面级请求与定时器自动管理(requestCancel.js mixin

  • 封装为 Vue mixin,页面只需 mixins: [requestCancelMixin] 即可。
  • 页面销毁时自动取消所有 pending 请求和定时器,防止内存泄漏。
  • 提供 safeApiCall 方法,自动处理异常和取消,页面只需 if (!res) return;,不用再写 catch
  • 提供 setSafeIntervalsetSafeTimeout,定时器自动管理,无需手动清理。

3. CRUD 相关健壮性优化(crud.js)

  • findVM、getTable 方法加了健壮性判断,防止 undefined 报错。
  • CRUD 组件在极端情况下也不会因找不到 VM 或 $refs 而崩溃。

4. 页面代码简化

  • 页面只需这样写即可:
      const res = await this.safeApiCall(apiFunc, params);
      if (!res) return;
      // 下面可以安全用 res.data
  • 不再需要 catch,也不会因取消请求而弹窗报错。

二、适用场景

1. 所有有接口请求的页面

  • 尤其是有定时刷新搜索切换等场景,强烈建议使用 requestCancelMixin

2. 有定时器的页面

  •  setSafeIntervalsetSafeTimeout,页面销毁时自动清理,防止内存泄漏。

3. 需要防止“接口请求失败”误报的场景

  • 比如页面切换、搜索防抖等,用户不会再因为请求被取消而看到报错弹窗。

4. 所有 CRUD 相关页面

  • 现在更健壮,极端情况下不会因找不到 VM 或 $refs 而崩溃。

三、带来的好处

  • 极大提升了系统健壮性和用户体验
  • 彻底解决了内存泄漏和误报错弹窗问题
  • 页面代码更简洁,维护成本更低
  • 全局和页面级请求管理分离,结构更清晰

四、推荐用法

  1. 页面顶部引入 mixin

   import requestCancelMixin from '@/mixins/requestCancel'
   export default {
     mixins: [requestCancelMixin],
     // ...
   }

 2.所有 API 调用用 safeApiCall 包裹:

   const res = await this.safeApiCall(apiFunc, params);
   if (!res) return;
   // ...

3.定时器用 setSafeIntervalsetSafeTimeout,不用手动清理。 

Vue 项目自动请求取消机制使用说明

一、核心文件

  • src/mixins/requestCancel.js(页面 mixin,自动管理请求和定时器)
  • src/utils/request.js(axios 封装,支持 pageId 取消令牌)

二、API 层改造(必须)

1. 每个 API 函数都加 config = {} 参数,并透传到 request:

// 推荐写法
export function getList(params, config = {}) {
  return request({
    url: '/api/list',
    method: 'get',
    params,
    ...config, // 关键!这样 pageId 能传递下去
  })
}

2. POST/PUT 也一样: 

export function saveData(data, config = {}) {
  return request({
    url: '/api/save',
    method: 'post',
    data,
    ...config,
  })
}

三、页面层用法

1. 引入 mixin:

import requestCancelMixin from '@/mixins/requestCancel'

export default {
  mixins: [requestCancelMixin],
  // ...
}

2. 所有 API 调用用 safeApiCall:

async getData() {
  const res = await this.safeApiCall(getList, { name: 'xxx' })
  if (!res) return // 被取消时直接退出
  // 正常处理数据
}

3.定时器用 setSafeInterval/setSafeTimeout: 

this.setSafeInterval(() => {
  this.getData()
}, 10000)

this.setSafeTimeout(() => {
  this.getData()
}, 5000)

4. 页面销毁时,所有请求和定时器会自动清理,无需手动 clear/cancel。 

四、常见问题与注意事项

  1. API 层必须支持 config 参数并 ...config,否则取消机制无效。
  1. safeApiCall 返回 null 代表请求被取消,页面逻辑需加 if (!res) return。
  1. 第三方/原生 fetch 请求不受此机制保护。
  1. 如有“不能被取消”的请求,直接用原始 await,不用 safeApiCall。

五、使用建议

  • API 层统一签名,所有接口都加 config = {},并 ...config。
  • 页面层统一用法,所有请求都用 safeApiCall,定时器用 setSafeInterval/setSafeTimeout。
  • 写一份团队文档/代码模板,新页面直接套用。

六、典型用法示例 

// api/user.js
export function getUserList(params, config = {}) {
  return request({
    url: '/api/user/list',
    method: 'get',
    params,
    ...config
  })
}

// 页面.vue
import { getUserList } from '@/api/user'
import requestCancelMixin from '@/mixins/requestCancel'

export default {
  mixins: [requestCancelMixin],
  methods: {
    async fetchUsers() {
      const res = await this.safeApiCall(getUserList, { page: 1 })
      if (!res) return
      this.users = res.data
    }
  },
  mounted() {
    this.fetchUsers()
    this.setSafeInterval(this.fetchUsers, 10000)
  }
}

七、效果

  • 页面销毁时,所有未完成请求和定时器自动清理,无内存泄漏、无“接口请求失败”弹窗。
  • 代码更简洁,无需手动管理定时器和请求取消。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

蓝胖子的多啦A梦

你的鼓励是我最大的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值