手写 Promise.map
代码实现
/**
* promiseMap:限制并发数量地执行异步任务
* @param {Array} list 任务列表,比如 url 数组、file 数组
* @param {Function} mapper 每一项要执行的异步函数,必须返回 Promise
* @param {number} limit 最大并发数
* @returns {Promise<Array>} 所有任务结果,顺序和原数组一致
*/
function promiseMap(list, mapper, limit) {
const results = [];
let index = 0;
let running = 0;
let stopped = false;
return new Promise((resolve, reject) => {
// 运行下一个任务
function runNext() {
// 如果已经停止执行,直接返回
if (stopped) {
return;
}
//所有任务都完成
if(index === list.length && running === 0) {
resolve(results);
return;
}
while(running < limit && index < list.length) {
const currentIndex = index;
const item = list[currentIndex];
index++;
running++;
Promise.resolve(mapper(item, currentIndex))
.then((result) => {
results[currentIndex] = result;
})
.catch((error) => {
stopped = true; // 停止执行后续任务
reject(error);
})
.finally(() => {
running--;
runNext();
});
}
}
// 开始执行任务
runNext();
})
}
//================测试
const urls = [1, 2, 3, 4, 5, 6];
function request(id) {
return new Promise((resolve) => {
setTimeout(() => {
console.log("完成请求", id);
resolve(id * 10);
}, 1000);
});
}
promiseMap(urls, request, 3).then((res) => {
console.log(res);
});
笔记记录
promiseMap
题目目标
实现一个带并发限制的 Promise.map:
- 输入一个任务列表
list - 对每一项执行异步函数
mapper(item, index) - 同一时间最多只能运行
limit个任务 - 所有任务成功后,按原数组顺序返回结果数组
- 任意一个任务失败后,整个
promiseMap失败
核心思路
维护一个任务池,最多同时跑 limit 个任务;每完成一个任务,就补一个新的任务进去。
需要记录几个状态:
index:下一个待启动任务的下标running:当前正在执行的任务数量results:保存每个任务的返回值,按原下标写入,保证结果顺序不乱stopped:是否已经失败;失败后不再启动新任务
执行流程
- 定义
runNext,负责启动后续任务。 - 每次进入
runNext时,先判断是否已经stopped。 - 如果
index === list.length && running === 0,说明没有待启动任务,也没有运行中的任务,可以resolve(results)。 - 只要
running < limit且还有任务没启动,就不断启动新任务。 - 启动任务时先保存
currentIndex,因为异步完成顺序不固定。 - 任务成功后,把结果写入
results[currentIndex]。 - 任务失败后,设置
stopped = true,并reject(error)。 - 任务结束后执行
finally:running--,然后再次调用runNext补位。
关键点
结果保序
任务完成顺序不一定等于任务启动顺序,所以不能直接 push 结果。
应该使用任务启动时保存的下标:
results[currentIndex] = result;这样即使第 3 个任务先完成,也会写回第 3 个位置。
并发控制
while (running < limit && index < list.length) 是并发控制的核心。
每启动一个任务:
index++;
running++;每结束一个任务:
running--;
runNext();这样任务池里空出一个位置后,就会自动补上一个新任务。
失败处理
如果任意任务失败:
stopped = true;
reject(error);stopped 的作用是避免失败后继续启动新的任务。
注意:已经启动的任务无法被普通 Promise 取消,它们仍然可能执行完成;这里只是不再补充新任务,并让外层 Promise 进入 rejected 状态。
边界情况
list为空时,第一次runNext会直接满足完成条件,返回空数组。limit应该是大于0的数字;如果limit <= 0,任务无法启动,Promise 会一直挂起。mapper可能同步抛错,也可能返回普通值,所以用Promise.resolve(mapper(...))统一包成 Promise。
伪代码
function promiseMap(list, mapper, limit) {
return new Promise((resolve, reject) => {
function runNext() {
if (已经失败) return;
if (所有任务都启动了 && 没有运行中的任务) {
resolve(results);
return;
}
while (当前运行数量 < limit && 还有任务没启动) {
启动一个任务;
任务成功: 按原下标保存结果;
任务失败: 标记停止并 reject;
任务结束: 运行数量减一,然后继续补任务;
}
}
runNext();
});
}