说真的,很多前端小伙伴在处理请求时,要么是“来者不拒”全并发,要么是“一个一个来”排队,结果要么把服务器打崩了,要么把用户体验拖慢了。今天咱们就坐下来,像老朋友聊天一样,把这Promise.all并发控制、队列管理和AbortController中断机制这三招给彻底讲透,顺便附上能直接跑的实战代码,保证你看完就能用在项目里。
第一招:Promise.all —— 并发控制的艺术
为什么我们需要它?
想象一下,你打开一个电商商品详情页,需要同时加载:
- 商品基本信息
- 用户评价
- 相关商品推荐
- 商家信息
如果一个个请求,用户可能要等好几秒。但如果无序并发,可能会瞬间发起几十个请求,把网络带宽撑爆,甚至触发后端的频率限制。
核心原理
Promise.all 本身很简单,它接收一个 Promise 数组,所有 Promise 都 resolve 时才 resolve,任意一个 reject 就立即 reject。但真正的并发控制需要配合一个“并发数量限制器”。
实战代码:带并发限制的 Promise.all
/**
* 并发控制的 Promise.all 实现
* @param {Array<Function>} tasks - 异步任务函数数组
* @param {number} concurrency - 最大并发数
* @returns {Promise<Array>} 所有任务结果的数组
*/
function PromiseAllWithConcurrency(tasks, concurrency) {
// 参数校验
if (!Array.isArray(tasks) || tasks.length === 0) {
return Promise.resolve([]);
}
if (concurrency <= 0) {
throw new Error('并发数必须大于0');
}
// 结果数组,用于保持原始顺序
const results = new Array(tasks.length);
let index = 0; // 当前要执行的task索引
let resolvedCount = 0; // 已完成的task数量
return new Promise((resolve, reject) => {
// 执行单个任务
const executeTask = async (taskFn, taskIndex) => {
try {
const result = await taskFn();
results[taskIndex] = result;
resolvedCount++;
// 所有任务完成
if (resolvedCount === tasks.length) {
resolve(results);
}
// 继续执行下一个任务
startNext();
} catch (error) {
reject(error);
}
};
// 启动下一个任务(如果还有)
const startNext = () => {
while (index < tasks.length && (index - resolvedCount) < concurrency) {
executeTask(tasks[index], index);
index++;
}
};
// 初始启动
startNext();
});
}
// ========== 使用示例 ==========
// 模拟5个API请求,最多同时并发3个
async function fetchProductInfo(id) {
return new Promise((resolve) => {
console.log(`[开始] 获取商品${id}信息`);
setTimeout(() => {
console.log(`[完成] 商品${id}信息`);
resolve({ id, name: `商品${id}`, price: 100 + id });
}, Math.random() * 1000 + 500);
});
}
async function fetchReviews(id) {
return new Promise((resolve) => {
console.log(`[开始] 获取商品${id}评价`);
setTimeout(() => {
console.log(`[完成] 商品${id}评价`);
resolve({ id, reviews: [`评价${id}-1`, `评价${id}-2`] });
}, Math.random() * 800 + 300);
});
}
async function fetchRecommendations(id) {
return new Promise((resolve) => {
console.log(`[开始] 获取商品${id}推荐`);
setTimeout(() => {
console.log(`[完成] 商品${id}推荐`);
resolve({ id, recommendations: [id + 1, id + 2] });
}, Math.random() * 600 + 200);
});
}
async function main() {
const productIds = [1, 2, 3];
// 构建任务数组:每个产品3个请求
const tasks = [];
productIds.forEach(id => {
tasks.push(() => fetchProductInfo(id));
tasks.push(() => fetchReviews(id));
tasks.push(() => fetchRecommendations(id));
});
console.log('=== 开始并发控制测试,最大并发数: 3 ===');
const startTime = Date.now();
try {
const results = await PromiseAllWithConcurrency(tasks, 3);
console.log('\n=== 所有请求完成 ===');
console.log(`耗时: ${Date.now() - startTime}ms`);
console.log('结果:', JSON.stringify(results, null, 2));
} catch (error) {
console.error('请求失败:', error);
}
}
main();
性能优化技巧
- 结果顺序保持:上面的实现保证了结果数组与原任务数组顺序一致,这很重要!
- 错误处理:任意一个任务失败,整个 Promise 立即 reject,适合“全有或全无”的场景
- 并发数选择:一般建议 3-10,具体看后端限制和用户需求
第二招:队列管理 —— 有序执行的优雅方案
场景分析
有些场景不适合全并发,比如:
- 用户连续快速点击“点赞”,需要按顺序执行,避免状态错乱
- 批量上传文件,需要控制队列防止内存溢出
- 日志上报,需要按时间顺序处理
核心实现:异步任务队列
/**
* 异步任务队列管理器
* 特点:
* 1. 任务按顺序执行
* 2. 支持设置最大并行数
* 3. 支持任务优先级(简单实现)
* 4. 提供队列状态监控
*/
class AsyncTaskQueue {
constructor(options = {}) {
this.maxConcurrency = options.maxConcurrency || 1;
this.tasks = [];
this.pendingTasks = [];
this.runningCount = 0;
this.isPaused = false;
this.isDestroyed = false;
this.callbacks = {
onTaskStart: options.onTaskStart || null,
onTaskComplete: options.onTaskComplete || null,
onTaskError: options.onTaskError || null,
onQueueEmpty: options.onQueueEmpty || null
};
}
/**
* 添加任务
* @param {Function} taskFn - 异步任务函数
* @param {Object} options - 任务选项
* @param {string} options.id - 任务唯一标识(用于取消)
* @param {number} options.priority - 优先级(数字越大越优先)
* @returns {Promise} 任务执行结果
*/
add(taskFn, options = {}) {
if (this.isDestroyed) {
throw new Error('队列已销毁,无法添加任务');
}
const task = {
fn: taskFn,
id: options.id || `task_${Date.now()}_${Math.random()}`,
priority: options.priority || 0,
startTime: Date.now(),
promise: null
};
// 根据优先级插入队列(简单实现:高优先级插前面)
if (this.tasks.length === 0) {
this.tasks.push(task);
} else {
let inserted = false;
for (let i = 0; i < this.tasks.length; i++) {
if (task.priority > this.tasks[i].priority) {
this.tasks.splice(i, 0, task);
inserted = true;
break;
}
}
if (!inserted) {
this.tasks.push(task);
}
}
// 创建任务Promise
task.promise = this._executeTask(task);
return task.promise;
}
/**
* 执行单个任务
*/
async _executeTask(task) {
if (this.isPaused) {
await new Promise(resolve => {
const checkResume = () => {
if (!this.isPaused) {
resolve();
} else {
setTimeout(checkResume, 100);
}
};
checkResume();
});
}
this.runningCount++;
this.tasks = this.tasks.filter(t => t !== task);
if (this.callbacks.onTaskStart) {
this.callbacks.onTaskStart(task);
}
const startTime = Date.now();
try {
const result = await task.fn();
const duration = Date.now() - startTime;
if (this.callbacks.onTaskComplete) {
this.callbacks.onTaskComplete(task, result, duration);
}
return result;
} catch (error) {
const duration = Date.now() - startTime;
if (this.callbacks.onTaskError) {
this.callbacks.onTaskError(task, error, duration);
}
throw error;
} finally {
this.runningCount--;
this._processQueue();
}
}
/**
* 处理队列,启动新任务
*/
_processQueue() {
while (this.runningCount < this.maxConcurrency && this.tasks.length > 0) {
const task = this.tasks.shift();
this._executeTask(task);
}
// 队列清空回调
if (this.tasks.length === 0 && this.runningCount === 0 && this.callbacks.onQueueEmpty) {
this.callbacks.onQueueEmpty();
}
}
/**
* 取消指定任务
*/
cancel(taskId) {
const index = this.tasks.findIndex(t => t.id === taskId);
if (index !== -1) {
this.tasks.splice(index, 1);
return true;
}
return false;
}
/**
* 清空队列
*/
clear() {
this.tasks = [];
this.runningCount = 0;
}
/**
* 暂停队列
*/
pause() {
this.isPaused = true;
}
/**
* 恢复队列
*/
resume() {
this.isPaused = false;
this._processQueue();
}
/**
* 销毁队列
*/
destroy() {
this.isDestroyed = true;
this.clear();
}
/**
* 获取队列状态
*/
getStatus() {
return {
pending: this.tasks.length,
running: this.runningCount,
maxConcurrency: this.maxConcurrency,
isPaused: this.isPaused,
isDestroyed: this.isDestroyed
};
}
}
// ========== 实战示例:批量图片上传队列 ==========
// 模拟上传API
function simulateUpload(file) {
return new Promise((resolve, reject) => {
const delay = Math.random() * 2000 + 500;
console.log(`[上传开始] ${file.name}, 预计${delay}ms`);
setTimeout(() => {
if (Math.random() > 0.1) { // 90%成功率
console.log(`[上传完成] ${file.name}`);
resolve({ url: `https://example.com/${file.name}`, size: file.size });
} else {
console.log(`[上传失败] ${file.name}`);
reject(new Error(`Upload failed for ${file.name}`));
}
}, delay);
});
}
// 创建队列实例
const uploadQueue = new AsyncTaskQueue({
maxConcurrency: 3, // 最多同时上传3个
onTaskStart: (task) => {
console.log(`[队列] 任务开始: ${task.id}`);
},
onTaskComplete: (task, result, duration) => {
console.log(`[队列] 任务完成: ${task.id}, 耗时: ${duration}ms`);
},
onTaskError: (task, error, duration) => {
console.log(`[队列] 任务失败: ${task.id}, 耗时: ${duration}ms, 错误: ${error.message}`);
},
onQueueEmpty: () => {
console.log('[队列] 所有任务完成!');
}
});
// 模拟10张图片上传
async function batchUpload() {
const files = Array.from({ length: 10 }, (_, i) => ({
name: `image_${i + 1}.jpg`,
size: Math.random() * 1000000 + 100000
}));
console.log('=== 开始批量图片上传 ===');
console.log(`文件大小: ${files.map(f => f.name).join(', ')}`);
const results = await Promise.all(
files.map(file =>
uploadQueue.add(() => simulateUpload(file), {
id: file.name,
priority: 1
})
)
);
console.log('\n=== 上传结果 ===');
console.log(`成功: ${results.length}`);
console.log('URL列表:', results.map(r => r.url));
}
batchUpload();
队列管理的优势
| 特性 | 说明 |
|---|---|
| 内存控制 | 避免大量任务同时占用内存 |
| 顺序保证 | 相同优先级的任务按顺序执行 |
| 可中断 | 支持随时取消任务 |
| 可监控 | 提供队列状态和性能数据 |
| 灵活配置 | 并发数、优先级、回调均可自定义 |
第三招:AbortController —— 精准中断请求的利器
为什么需要中断?
- 用户快速切换路由:上一个页面的请求还在进行,浪费资源
- 请求超时控制:某些请求长时间无响应需要取消
- 用户主动取消:比如表单提交时用户点击取消按钮
- 竞态条件解决:后发送的请求优先,先发送的需要取消
基础用法
// 最简单的AbortController用法
const controller = new AbortController();
const signal = controller.signal;
fetch('https://api.example.com/data', { signal })
.then(response => response.json())
.then(data => console.log('成功:', data))
.catch(error => {
if (error.name === 'AbortError') {
console.log('请求被主动取消');
} else {
console.error('请求失败:', error);
}
});
// 3秒后取消请求
setTimeout(() => {
controller.abort();
}, 3000);
实战案例:搜索框防抖 + 请求取消
这是最经典的 AbortController 应用场景:
”`javascript /**
搜索框请求管理
功能:
1. 防抖处理(避免频繁请求)
2. 自动取消旧请求(后发的请求优先)
3. 请求超时控制
-
- 错误重试机制 */ class SearchController { constructor(options = {}) { this.controller = null; this.timeout = options.timeout || 5000; this.retryCount = options.retryCount || 2; this.debounceDelay = options.debounceDelay || 300; this.lastQuery = null; this.pendingRequests = new Map();
this.callbacks = { onSearchStart: options.onSearchStart || null, onSearchSuccess: options.onSearchSuccess || null, onSearchError: options.onSearchError || null, onSearchCancel: options.onSearchCancel || null }; }
/**
执行搜索(防抖 + 取消旧请求)
@param {string} query - 搜索关键词
@returns {Promise
} 搜索结果 */ async search(query) { // 清除之前的定时器 if (this.debouceTimer) { clearTimeout(this.debouceTimer); } // 防抖 return new Promise((resolve, reject) => { this.debouceTimer = setTimeout(async () => {
await this._executeSearch(query); resolve();}, this.debounceDelay); }); }
/**
执行实际搜索逻辑 */ async _executeSearch(query) { // 取消之前的请求 this._cancelPendingRequest();
// 创建新的控制器 this.controller = new AbortController(); const signal = this.controller.signal; this.lastQuery = query;
if (this.callbacks.onSearchStart) { this.callbacks.onSearchStart(query); }
const requestId =
search_${Date.now()}; this.pendingRequests.set(requestId, { controller: this.controller, query: query, startTime: Date.now() });try { const result = await this._fetchWithRetry(query, signal);
if (this.callbacks.onSearchSuccess) {
this.callbacks.onSearchSuccess(query, result);}
return result; } catch (error) { if (error.name === ‘AbortError’) {
if (this.callbacks.onSearchCancel) { this.callbacks.onSearchCancel(query); }} else {
if (this.callbacks.onSearchError
