手写Promise.all与并发调度器深度解析
一句话概括
手写 Promise.all / Promise.race / Promise.allSettled 的底层只用了一个计数器 + 一个 settled 标志,而并发调度器不过是在这层机制上套了一个 while (running < max && queue.length) 的消费循环——理解这四个字就理解了 Promise 组合的全部。
核心知识点
① Promise.all:计数器决定何时 resolve
全成功才成功,一失败即拒绝。核心只有两点:结果按索引写入(不是按完成顺序) + 计数器满即 resolve。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function myAll(iterable) {
return new Promise((resolve, reject) => {
const arr = [...iterable];
if (arr.length === 0) return resolve([]);
const results = new Array(arr.length);
let count = 0; // ← 这就是核心
arr.forEach((item, i) => {
Promise.resolve(item).then(
val => {
results[i] = val; // 写入对应索引,不是 push
if (++count === arr.length) resolve(results);
},
reason => reject(reason) // 谁先挂,整个 all 就挂
);
});
});
}
// 验证
myAll([Promise.resolve(1), 2, Promise.resolve(3)]).then(console.log); // [1,2,3]
myAll([Promise.reject('boom'), Promise.resolve(1)]).catch(console.log); // 'boom'
② Promise.race:谁先 settled 谁赢
race 没有计数器,只有一个 settled 锁——第一个落地的结果一把锁锁死,后面的全部忽略。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function myRace(iterable) {
return new Promise((resolve, reject) => {
const arr = [...iterable];
if (arr.length === 0) return; // 空 → 永远 pending
let settled = false;
arr.forEach(item => {
Promise.resolve(item).then(
val => { if (!settled) { settled = true; resolve(val); } },
err => { if (!settled) { settled = true; reject(err); } }
);
});
});
}
// 验证
myRace([
new Promise(r => setTimeout(() => r('slow'), 500)),
new Promise(r => setTimeout(() => r('fast'), 50)),
]).then(console.log); // 'fast'
③ Promise.allSettled:永远不 reject 的 all
跟 myAll 几乎一样,唯一区别:.then() 的两个分支都记录结果并 ++count,resolve 和 reject 殊途同归。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function myAllSettled(iterable) {
return new Promise(resolve => {
const arr = [...iterable];
if (arr.length === 0) return resolve([]);
const results = new Array(arr.length);
let count = 0;
arr.forEach((item, i) => {
Promise.resolve(item).then(
value => { results[i] = { status: 'fulfilled', value }; if (++count === arr.length) resolve(results); },
reason => { results[i] = { status: 'rejected', reason }; if (++count === arr.length) resolve(results); }
);
});
});
}
// 验证
myAllSettled([Promise.resolve(1), Promise.reject('err'), Promise.resolve(3)])
.then(r => console.log(r.map(x => x.status))); // ['fulfilled', 'rejected', 'fulfilled']
④ 并发调度器:while 循环 + 任务队列
调度器本质是一个消费者循环:只要并发没满且队列非空,就出队执行。任务完成后 finally 里递归调用自己继续消费。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class ConcurrencyScheduler {
constructor(limit = 3) {
this.limit = limit;
this.queue = [];
this.running = 0;
}
add(task) {
return new Promise((resolve, reject) => {
this.queue.push({ task, resolve, reject });
this.#next();
});
}
#next() {
while (this.running < this.limit && this.queue.length) {
const { task, resolve, reject } = this.queue.shift();
this.running++;
Promise.resolve(task())
.then(resolve, reject)
.finally(() => { this.running--; this.#next(); });
}
}
}
// 验证:5个任务并发3个 → 1/2/3 同时启动,300ms后 4/5 接着启动
const s = new ConcurrencyScheduler(3);
for (let i = 1; i <= 5; i++) s.add(() => new Promise(r => setTimeout(() => r(i), 300)));
⑤ Promise.any:与 all 镜像对称(ES2021)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function myAny(iterable) {
return new Promise((resolve, reject) => {
const arr = [...iterable];
if (arr.length === 0) return reject(new AggregateError([], 'All rejected'));
const errors = new Array(arr.length);
let count = 0;
arr.forEach((item, i) =>
Promise.resolve(item).then(resolve, err => {
errors[i] = err;
if (++count === arr.length) reject(new AggregateError(errors, 'All rejected'));
})
);
});
}
其实你每天都在用
① 首屏数据聚合。 同时请求用户信息、购物车、消息未读、首页推荐,等全部返回再渲染 → Promise.all。只关心最快显示一块内容 → Promise.race。
② 图片批量上传。 朋友圈选 9 张图,后端限制同时 3 个连接。不能无脑发 9 个请求——并发调度器上场:3 个一批,3 个一批,有序消耗连接。
③ 请求降级与超时。 同时请求主备两个支付接口,哪个先返回用哪个 → Promise.race。必须等所有子服务返回再汇总成败 → Promise.allSettled。
④ 文件分片下载。 大文件切 10 个 chunk,浏览器同域名 TCP 连接上限 6,用调度器控制在 4 并发——超了 TCP 窗口也扛不住。
⑤ 表单多字段校验。 用户名/手机号/邮箱三个异步校验,任何一个失败就阻止提交 → Promise.all。
常见误解
❌ 误区:Promise.all 里一个 reject,其他 Promise 会自动取消。
不会。Promise 一旦创建就无法取消,其他任务会照常执行到底,只是外层 all 已经 reject 了,它们的结果被丢弃。真正需要取消能力的,要用 AbortController 或任务内部检查取消标志。
❌ 误区:Promise.all([p1, p2, p3]) 结果数组的顺序是完成顺序。
不是。结果按输入顺序排列,跟谁先跑完无关。你可以在 p1.then 里记 results[0]、p2.then 里记 results[1]——索引由传入时的位置决定,不是按 .then() 触发的先后。
❌ 误区:Promise.race([]) 会 reject 或返回 null。
不会。空数组的 race 会永远 pending——因为没有任何 Promise 来触发 resolve 或 reject。这是一个经典的面试陷阱:Promise.all([]) → 立即 resolved [],Promise.race([]) → 永远悬停。
❌ 误区:调度器里用 setInterval 轮询队列最方便。
轮询是浪费。正确的做法是事件驱动:每个任务在 .finally() 里调用 #next() 自动触发下一个,没有任务跑的时候什么都不做,CPU 零消耗。setInterval 即使在空闲时也在消耗 CPU 时钟周期。
一句话总结
Promise 组合方法的本质不是一种 API,而是一种状态机——计数器追踪进度,settled 锁防止竞态,while 循环消费队列;手写完这三个模式,你对 JavaScript 异步的理解就跨越了”会用 API”到”理解事件循环”的分水岭。