前端接口并发方案

365 阅读6分钟

一、方案背景

在前端中经常会出现多接口并发的场景,而针对请求常规方式会一股脑发送给浏览器,浏览器中会对大量请求进行排队,但大量请求推送至浏览器就会占用内存,最后达到一定量级导致页面卡顿,甚至假死常规请求图如下

c2d85921-25ea-472e-a8a0-77aaed16111e.png

二、解决思路

针对此模式,我们可以创建如下序列,待发送请求池作为中间层,只作为载体,每次推送固定数量到浏览器,当浏览器主线程中存在接口执行完毕后,代表主线程中接口数量减少,在从待发送请求中取出新的接口放入主线程,从而实现控制主线程发送数量避免内存溢出

  • 创建待发送请求池
  • 控制最大并行请求数量
  • 指定接口缓存,如数据源,结合请求,响应拦截器进行处理

流程图如下

f18ca013-a1b6-40db-a63f-25bba726c0a2.png

三、具体代码实现

请求并发控制方案

概述

这是一个基于请求池(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++) {
  // 批量启动
}

注意事项

  1. 并发数设置:默认 20 个,可根据实际情况调整
  2. 请求取消:路由切换或组件卸载时记得取消请求
  3. 错误处理:已取消的请求不会触发 reject,避免重复处理
  4. 缓存功能:当前代码中已预留,可根据需要启用
  5. 浏览器限制:不同浏览器对同域名并发请求有限制(通常 6-8 个),但通过请求池可以更好地控制