前言
目录
3. 全局对象引用 (这个看个人需求,因为我的页面还使用到了国际化语言,所以有这方面问题)
1.创建一个Vue mixin来自动处理页面级别的请求取消。
2. 页面级请求与定时器自动管理(requestCancel.js mixin)
3.定时器用 setSafeInterval、setSafeTimeout,不用手动清理。
项目中 多个页面都有使用到定时器,调用频率在 每秒掉一次接口,最近长时间运行页面,会出现明显的卡顿 甚至浏览器崩溃,一直在排查,浏览器的Performance、Memory监控内存,快照监控,各种小工具进行排查,也看到了网上很多 博文 说是:页面少用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()
- 避免全局对象引用,提高垃圾回收效率
思考
取消机制是否必要?
场景分析:
- 快速切换页面 - 用户快速点击导航,页面A的请求还没完成就切换到页面B
- 组件频繁创建销毁 - 如弹窗、标签页等
- 搜索/筛选操作 - 用户快速输入,每次输入都触发请求
- 定时刷新 - 页面有定时器不断发送请求
不取消请求的后果:
- 内存泄露 - 未完成的Promise会一直占用内存
- 网络资源浪费 - 无用的请求继续占用带宽
- 数据竞争 - 后发的请求可能比先发的请求先返回
- 用户体验差 - 可能显示过期的数据
📊 分析结果
在我的项目里确实非常需要取消机制,原因如下:
🚨 高风险场景:
- 大量定时器 - 项目中有50+个页面使用 setInterval,很多是1秒间隔的实时刷新
- 频繁搜索 - 大量页面有 @keyup.enter.native="crud.toQuery" 搜索功能
- 实时监控 - 多个看板页面有定时刷新机制
💡 具体问题示例(我的页面):
// 这些页面都有定时器,如果不取消请求会导致严重的内存泄露
this.timer = setInterval(() => {
setTimeout(async () => {
this.getList() // 每秒都在发送请求
}, 0);
}, 1000);
结论:取消机制非常必要!
为什么必要:
- 防止内存泄露 - 定时器页面切换时,pending请求会一直占用内存
- 避免数据竞争 - 快速搜索时,后发的请求可能先返回
- 节省网络资源 - 取消无用的请求,减少服务器压力
- 提升用户体验 - 避免显示过期数据
建议的实施策略:
- 立即实施 - 对于有定时器的页面
- 逐步推广 - 对于搜索频繁的页面
- 全局配置 - 使用我们刚才创建的mixin
优先级排序:
- 高优先级 - 有定时器的看板页面
- 中优先级 - 有搜索功能的列表页面
- 低优先级 - 静态页面
总结
取消机制不是可选的,而是必需的!在我的项目中,有这么多实时刷新的页面,没有取消机制会导致严重的内存泄露和性能问题。
📝 本次前端请求与内存优化总结
一、主要修改内容
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。
- 提供 setSafeInterval、setSafeTimeout,定时器自动管理,无需手动清理。
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. 有定时器的页面
- 用 setSafeInterval、setSafeTimeout,页面销毁时自动清理,防止内存泄漏。
3. 需要防止“接口请求失败”误报的场景
- 比如页面切换、搜索防抖等,用户不会再因为请求被取消而看到报错弹窗。
4. 所有 CRUD 相关页面
- 现在更健壮,极端情况下不会因找不到 VM 或 $refs 而崩溃。
三、带来的好处
- 极大提升了系统健壮性和用户体验
- 彻底解决了内存泄漏和误报错弹窗问题
- 页面代码更简洁,维护成本更低
- 全局和页面级请求管理分离,结构更清晰
四、推荐用法
-
页面顶部引入 mixin:
import requestCancelMixin from '@/mixins/requestCancel'
export default {
mixins: [requestCancelMixin],
// ...
}
2.所有 API 调用用 safeApiCall 包裹:
const res = await this.safeApiCall(apiFunc, params);
if (!res) return;
// ...
3.定时器用 setSafeInterval、setSafeTimeout,不用手动清理。
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。
四、常见问题与注意事项
- API 层必须支持 config 参数并 ...config,否则取消机制无效。
- safeApiCall 返回 null 代表请求被取消,页面逻辑需加 if (!res) return。
- 第三方/原生 fetch 请求不受此机制保护。
- 如有“不能被取消”的请求,直接用原始 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)
}
}
七、效果
- 页面销毁时,所有未完成请求和定时器自动清理,无内存泄漏、无“接口请求失败”弹窗。
- 代码更简洁,无需手动管理定时器和请求取消。