一、问题本质与核心挑战
需求:将100张图片上传至服务器,但服务器限制最多5个并发请求,需设计前端并发控制方案。
核心挑战:
- 控制并发数不超过5,避免服务器拒绝请求;
- 处理请求队列,确保所有图片按顺序或合理策略上传;
- 优化用户体验(进度展示、错误重试)。
二、并发控制的三种实现方案
1. 自定义并发控制函数(推荐方案)
通过维护任务队列和执行中的任务数,实现精确的并发控制:
function uploadImage(file) {
return new Promise((resolve, reject) => {
// 模拟上传,随机延迟100-500ms
const delay = 100 + Math.random() * 400;
setTimeout(() => {
if (Math.random() < 0.1) {
// 10%概率模拟失败
reject(new Error(`上传失败: ${file.name}`));
} else {
resolve(`成功: ${file.name}`);
}
}, delay);
});
}
/**
* 并发控制函数
* @param tasks 任务数组(返回Promise的函数)
* @param concurrency 最大并发数
*/
async function controlConcurrency(tasks, concurrency = 5) {
const results = [];
const executing = new Set(); // 存储正在执行的任务
// 执行单个任务
async function executeTask(task, index) {
const taskPromise = task();
executing.add(taskPromise);
try {
const result = await taskPromise;
results[index] = { success: true, data: result };
return result;
} catch (error) {
results[index] = { success: false, error: error.message };
throw error;
} finally {
executing.delete(taskPromise);
// 执行下一个任务
runNextTask();
}
}
// 运行下一个任务
function runNextTask() {
const nextTask = tasks.shift();
if (nextTask && executing.size < concurrency) {
executeTask(nextTask, results.length);
}
}
// 启动初始任务
while (executing.size < concurrency && tasks.length > 0) {
runNextTask();
}
// 等待所有任务完成
await Promise.all(executing);
return results;
}
// 使用示例
const imageFiles = Array(100).fill(null).map((_, i) => ({ name: `image${i}.jpg` }));
const tasks = imageFiles.map(file => () => uploadImage(file));
controlConcurrency(tasks, 5)
.then(results => {
console.log('所有图片上传完成', results);
// 统计成功/失败数量
const successCount = results.filter(r => r.success).length;
console.log(`成功: ${successCount}/${imageFiles.length}`);
})
.catch(error => {
console.error('上传过程中发生错误', error);
});
2. 使用Async/Await配合队列(简洁方案)
利用JavaScript异步迭代器和队列实现并发控制:
async function uploadImages(images, concurrency = 5) {
const results = [];
const queue = [...images];
const running = new Set();
// 执行上传任务
async function processImage() {
if (queue.length === 0 && running.size === 0) {
return results;
}
// 取出下一个图片
const image = queue.shift();
if (!image) return processImage();
// 执行上传
const task = uploadImage(image).then(
data => {
results.push({ success: true, data });
return data;
},
error => {
results.push({ success: false, error: error.message });
throw error;
}
).finally(() => {
running.delete(task);
processImage(); // 处理下一个任务
});
running.add(task);
return task;
}
// 启动多个并发任务
const initialTasks = Array(concurrency).fill().map(processImage);
return Promise.all(initialTasks);
}
3. 使用第三方库(如p-queue)
借助成熟的并发控制库简化实现:
import PQueue from 'p-queue';
// 上传函数
function uploadImage(file) {
return new Promise((resolve, reject) => {
// 模拟上传逻辑
setTimeout(() => {
resolve(`上传成功: ${file.name}`);
}, 100 + Math.random() * 300);
});
}
// 批量上传
async function batchUpload(images) {
// 创建队列,最大并发数5
const queue = new PQueue({
concurrency: 5,
autoStart: true
});
const results = [];
// 添加任务到队列
images.forEach((image, index) => {
queue.add(() =>
uploadImage(image).then(
data => {
results[index] = { success: true, data };
return data;
},
error => {
results[index] = { success: false, error: error.message };
throw error;
}
)
);
});
// 等待所有任务完成
await queue.onIdle();
return results;
}
// 使用示例
const imageFiles = Array(100).fill(null).map((_, i) => ({ name: `img${i}.png` }));
batchUpload(imageFiles)
.then(results => {
console.log(`成功上传: ${results.filter(r => r.success).length}/100`);
});
三、问题
1. 问:为什么不使用Promise.all直接上传?
- 答:
- Promise.all 会同时执行所有100个任务,远超服务器限制的5个并发,导致:
- 大量请求被服务器拒绝(429 Too Many Requests);
- 客户端内存占用过高,可能导致浏览器卡顿;
- 网络资源竞争,实际上传速度反而变慢。
- 并发控制的核心是平滑控制请求频率,避免资源浪费和服务器过载。
- Promise.all 会同时执行所有100个任务,远超服务器限制的5个并发,导致:
2. 问:如何优化大文件上传的并发策略?
- 答:
- 动态调整并发数:根据网络状况自动降低并发(如通过
navigator.connection
检测网络类型); - 优先级队列:重要图片(如封面图)优先上传,普通图片后排队;
- 分片上传:大文件拆分为多个分片,每个分片作为独立任务加入队列;
- 断点续传:结合并发控制,支持中断后从上次完成的分片继续上传。
- 动态调整并发数:根据网络状况自动降低并发(如通过
3. 问:如何实现上传进度的实时展示?
- 答:
- 任务计数:通过
完成数/总数
计算总体进度; - 单个任务进度:利用
XMLHttpRequest.upload.onprogress
事件获取单个文件的上传进度; - 进度合并:维护一个进度对象,实时更新每个文件的进度和总体进度:
const progress = { total: images.length, completed: 0, details: {} // { fileId: { loaded, total, percent } } };
- 任务计数:通过
四、实战优化:错误处理与用户体验
1. 错误重试机制
function uploadImageWithRetry(file, retries = 3) {
return new Promise((resolve, reject) => {
const attempt = async (retry = 0) => {
try {
const result = await uploadImage(file);
resolve(result);
} catch (error) {
if (retry < retries) {
console.log(`重试上传: ${file.name} (${retry + 1}/${retries})`);
setTimeout(() => attempt(retry + 1), 1000 * (retry + 1)); // 指数退避
} else {
reject(new Error(`上传失败(${retries}次重试后): ${file.name}`));
}
}
};
attempt();
});
}
2. 取消上传功能
function controllableUpload(file) {
let isCancelled = false;
return {
promise: new Promise((resolve, reject) => {
const task = uploadImage(file)
.then(resolve)
.catch(reject);
// 取消逻辑
this.cancel = () => {
isCancelled = true;
// 这里可添加取消上传的逻辑(如AbortController)
};
}),
cancel() {
isCancelled = true;
}
};
}
五、总结
“处理100张图片的并发上传需解决三个核心问题:
- 并发控制:通过任务队列和执行中任务计数,确保同时运行的上传任务不超过5个;
- 队列管理:维护待执行任务列表,当前任务完成后自动触发下一个任务;
- 体验优化:添加错误重试、进度展示和取消功能,提升用户体验。