一、方案背景
在前端中经常会出现多接口并发的场景,而针对请求常规方式会一股脑发送给浏览器,浏览器中会对大量请求进行排队,但大量请求推送至浏览器就会占用内存,最后达到一定量级导致页面卡顿,甚至假死常规请求图如下
二、解决思路
针对此模式,我们可以创建如下序列,待发送请求池作为中间层,只作为载体,每次推送固定数量到浏览器,当浏览器主线程中存在接口执行完毕后,代表主线程中接口数量减少,在从待发送请求中取出新的接口放入主线程,从而实现控制主线程发送数量避免内存溢出
- 创建待发送请求池
- 控制最大并行请求数量
- 指定接口缓存,如数据源,结合请求,响应拦截器进行处理
流程图如下
三、具体代码实现
请求并发控制方案
概述
这是一个基于请求池(Request Pool)的并发控制方案,用于管理前端应用中的 HTTP 请求。通过双队列机制控制并发数量,避免同时发起过多请求导致的性能问题和浏览器限制。
核心特性
- 并发控制:限制同时执行的请求数量(默认 20 个)
- 双队列设计:待执行队列(pendingQueue)+ 执行中队列(runningQueue)
- 请求取消:支持取消单个或全部请求,基于 AbortController
- 缓存支持:可配置的请求结果缓存机制
- 内存优化:请求完成后自动清理引用,防止内存泄漏
- 非阻塞调度:使用
setTimeout(0)让出主线程,避免阻塞 UI - 防重复调度:通过
isProcessing标志位避免重复调度
架构设计
1. 状态管理
const poolState = {
pendingQueue: [], // 待执行队列
runningQueue: [], // 执行中队列
cacheStore: new Map(), // 缓存存储
maxConcurrent: 20, // 最大并发数
maxPendingQueue: Infinity, // 待执行队列最大长度
isProcessing: false // 防止重复调度标志位
};
2、核心机制
防重复调度
使用 isProcessing 标志位防止多次调用 scheduleExecute() 时重复创建定时器:
const scheduleExecute = () => {
if (poolState.isProcessing) return; // 已在处理中,直接返回
poolState.isProcessing = true;
setTimeout(() => {
processQueue();
poolState.isProcessing = false; // 处理完成,重置标志
}, 0);
};
批量处理
每次调度时,根据可用槽位批量启动请求:
const processQueue = () => {
// 计算可用槽位
const slots = poolState.maxConcurrent - poolState.runningQueue.length;
const count = Math.min(slots, poolState.pendingQueue.length);
// 批量启动
for (let i = 0; i < count; i++) {
const task = poolState.pendingQueue.shift();
if (task) {
poolState.runningQueue.push(task);
executeRequest(task);
}
}
};
API 文档
addRequest(config, axiosInstance)
添加请求到队列
参数:
config(Object): axios 请求配置对象axiosInstance(Function): axios 实例
返回:
Promise: 请求的 Promise 对象
示例:
import axios from 'axios';
import { addRequest } from '@/utils/requestPool';
const config = {
method: 'get',
url: '/api/users',
params: { page: 1 }
};
addRequest(config, axios)
.then(response => console.log(response.data))
.catch(error => console.error(error));
setMaxConcurrent(max)
设置最大并发数
参数:
max(Number): 最大并发数
示例:
import { setMaxConcurrent } from '@/utils/requestPool';
// 设置最大并发数为 10
setMaxConcurrent(10);
clearAllRequests()
取消所有请求(包括待执行和执行中的请求)
返回:
Object: 取消的请求数量统计pending(Number): 待执行队列中取消的数量running(Number): 执行中队列中取消的数量
示例:
import { clearAllRequests } from '@/utils/requestPool';
const count = clearAllRequests();
console.log(`取消了 ${count.pending} 个待执行请求`);
console.log(`取消了 ${count.running} 个执行中请求`);
getPoolState()
获取请求池当前状态(用于调试和监控)
返回:
Object: 请求池状态信息
示例:
import { getPoolState } from '@/utils/requestPool';
const state = getPoolState();
console.log('待执行:', state.pendingCount);
console.log('执行中:', state.runningCount);
console.log('缓存数:', state.cacheCount);
getCacheKey(config)
生成缓存键(预留功能)
参数:
config(Object): axios 配置对象,需包含cacheParams字段
返回:
String | null: 缓存键,如果未配置cacheParams则返回 null
getCache(key) / setCache(key, data) / clearCache(key)
缓存操作方法(预留功能)
使用示例
1. 基础集成 - Axios 拦截器
在 axios 实例中集成请求池:
// src/utils/request.js
import axios from 'axios';
import { addRequest, clearAllRequests } from './requestPool';
const service = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
});
// 请求拦截器
service.interceptors.request.use(
config => {
// 标记使用请求池
config.usePool = true;
return config;
},
error => Promise.reject(error)
);
// 响应拦截器
service.interceptors.response.use(
response => response.data,
error => {
console.error('请求失败:', error);
return Promise.reject(error);
}
);
// 重写 axios 请求方法,使用请求池
const originalRequest = service.request.bind(service);
service.request = function(config) {
if (config.usePool !== false) {
// 使用请求池
return addRequest(config, originalRequest);
}
// 不使用请求池,直接请求
return originalRequest(config);
};
export default service;
export { clearAllRequests };
2. API 调用示例
// src/api/user.js
import request from '@/utils/request';
// 获取用户列表
export const getUserList = (params) => {
return request({
url: '/api/users',
method: 'get',
params
});
};
// 获取用户详情
export const getUserDetail = (id) => {
return request({
url: `/api/users/${id}`,
method: 'get'
});
};
// 创建用户
export const createUser = (data) => {
return request({
url: '/api/users',
method: 'post',
data
});
};
3. 组件中使用
<template>
<div>
<button @click="loadData">加载数据</button>
<button @click="cancelAll">取消所有请求</button>
<div>待执行: {{ poolState.pendingCount }}</div>
<div>执行中: {{ poolState.runningCount }}</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { getUserList } from '@/api/user';
import { clearAllRequests, getPoolState } from '@/utils/requestPool';
const poolState = ref({
pendingCount: 0,
runningCount: 0
});
// 更新请求池状态
const updatePoolState = () => {
const state = getPoolState();
poolState.value = state;
};
// 加载数据
const loadData = async () => {
try {
// 同时发起 50 个请求,但只有 20 个会并发执行
const promises = Array.from({ length: 50 }, (_, i) =>
getUserList({ page: i + 1 })
);
// 定时更新状态
const timer = setInterval(updatePoolState, 100);
const results = await Promise.all(promises);
clearInterval(timer);
console.log('所有请求完成:', results.length);
} catch (error) {
console.error('请求失败:', error);
}
};
// 取消所有请求
const cancelAll = () => {
const count = clearAllRequests();
console.log(`已取消 ${count.pending + count.running} 个请求`);
updatePoolState();
};
onMounted(() => {
updatePoolState();
});
</script>
4. 路由切换时取消请求
// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
import { clearAllRequests } from '@/utils/requestPool';
const router = createRouter({
history: createWebHistory(),
routes: [
// 路由配置...
]
});
// 路由切换前取消所有请求
router.beforeEach((to, from, next) => {
if (from.path !== '/') {
const count = clearAllRequests();
console.log(`路由切换,取消了 ${count.pending + count.running} 个请求`);
}
next();
});
export default router;
5. 批量请求示例
// 批量加载用户详情
async function loadUserDetails(userIds) {
const promises = userIds.map(id => getUserDetail(id));
try {
const results = await Promise.all(promises);
console.log('加载完成:', results);
return results;
} catch (error) {
console.error('批量加载失败:', error);
throw error;
}
}
// 使用示例
const userIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
loadUserDetails(userIds);
6. 动态调整并发数
import { setMaxConcurrent, getPoolState } from '@/utils/requestPool';
// 根据网络状况动态调整
function adjustConcurrency() {
const connection = navigator.connection;
if (connection) {
const effectiveType = connection.effectiveType;
switch (effectiveType) {
case 'slow-2g':
case '2g':
setMaxConcurrent(5);
break;
case '3g':
setMaxConcurrent(10);
break;
case '4g':
default:
setMaxConcurrent(20);
break;
}
console.log('网络类型:', effectiveType);
console.log('当前并发数:', getPoolState().maxConcurrent);
}
}
// 监听网络变化
if (navigator.connection) {
navigator.connection.addEventListener('change', adjustConcurrency);
}
7. 请求优先级(扩展示例)
如果需要实现请求优先级,可以扩展 addRequest 方法:
// 扩展版本 - 支持优先级
export const addRequestWithPriority = (config, axiosInstance, priority = 0) => {
return new Promise((resolve, reject) => {
const abortController = new AbortController();
const task = {
config: { ...config, signal: abortController.signal },
resolve,
reject,
axiosInstance,
abortController,
priority // 添加优先级字段
};
// 根据优先级插入队列
const index = poolState.pendingQueue.findIndex(t => t.priority < priority);
if (index === -1) {
poolState.pendingQueue.push(task);
} else {
poolState.pendingQueue.splice(index, 0, task);
}
scheduleExecute();
});
};
// 使用示例
addRequestWithPriority(config, axios, 10); // 高优先级
addRequestWithPriority(config, axios, 0); // 普通优先级
性能优化要点
1. 内存管理
请求完成后自动清理任务对象的引用:
task.config = null;
task.resolve = null;
task.reject = null;
task.axiosInstance = null;
task.abortController = null;
2. 非阻塞调度
使用 setTimeout(0) 让出主线程:
setTimeout(() => {
processQueue();
poolState.isProcessing = false;
}, 0);
3. 批量处理
一次调度处理多个请求,减少调度次数:
const count = Math.min(slots, poolState.pendingQueue.length);
for (let i = 0; i < count; i++) {
// 批量启动
}
注意事项
- 并发数设置:默认 20 个,可根据实际情况调整
- 请求取消:路由切换或组件卸载时记得取消请求
- 错误处理:已取消的请求不会触发 reject,避免重复处理
- 缓存功能:当前代码中已预留,可根据需要启用
- 浏览器限制:不同浏览器对同域名并发请求有限制(通常 6-8 个),但通过请求池可以更好地控制