怎样手写 Promise.race、Promise.allSettled 和 Promise.any?
三者都返回一个新 Promise,遍历输入并用 Promise.resolve 包装每一项。race 跟随第一个敲定的结果;allSettled 等全部敲定后按输入顺序返回状态对象;any 取第一个成功的值,全部失败时用 AggregateError 拒绝。空数组时 race 永远 pending,allSettled 兑现为空数组,any 以 AggregateError 拒绝。
简答
给接口请求加超时,常见写法是
Promise.race([fetch(url), timeout(3000)]),谁先有结果就用谁。手写race、allSettled、any的思路都一样:返回一个新 Promise,遍历输入,用Promise.resolve包装每一项,再决定什么时候 resolve 或 reject。三者的区别在于等什么。
race跟随第一个敲定(成功或失败)的结果;allSettled等所有项都敲定,按输入顺序返回{ status, value }或{ status, reason },自己永远不会失败;any取第一个成功的值,只有全部失败时才拒绝,拒绝原因是AggregateError,它的errors属性按输入顺序放着每个失败原因。空数组是常被问到的边界,三者各不相同。MDN 写明:
race([])永远处于 pending;allSettled([])直接兑现为[];any([])直接以AggregateError拒绝。要注意,这几个方法敲定以后不会取消其他还在进行的操作。
race做超时只是不再等慢的那个请求,请求本身还在跑,真要中断得配合AbortController。
三个方法的实现
下面的实现重点看两处:怎样按输入顺序保存结果,以及怎样判断“全部完成”。
function race(iterable) {
return new Promise((resolve, reject) => {
for (const item of iterable) {
Promise.resolve(item).then(resolve, reject);
}
});
}
function allSettled(iterable) {
return new Promise((resolve) => {
const results = [];
let count = 0;
let index = 0;
for (const item of iterable) {
const i = index++;
Promise.resolve(item).then(
(value) => {
results[i] = { status: 'fulfilled', value };
if (++count === index) resolve(results);
},
(reason) => {
results[i] = { status: 'rejected', reason };
if (++count === index) resolve(results);
},
);
}
if (index === 0) resolve(results);
});
}
function any(iterable) {
return new Promise((resolve, reject) => {
const errors = [];
let count = 0;
let index = 0;
for (const item of iterable) {
const i = index++;
Promise.resolve(item).then(resolve, (reason) => {
errors[i] = reason;
if (++count === index) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
}
if (index === 0) {
reject(new AggregateError(errors, 'All promises were rejected'));
}
});
}
const delay = (ms, value, fail = false) =>
new Promise((resolve, reject) => setTimeout(() => (fail ? reject(value) : resolve(value)), ms));
async function main() {
console.log(await race([delay(30, '慢'), delay(10, '快')])); // 快
console.log(await allSettled([delay(20, 'A'), delay(10, '坏', true), 3]));
// [
// { status: 'fulfilled', value: 'A' },
// { status: 'rejected', reason: '坏' },
// { status: 'fulfilled', value: 3 }
// ]
console.log(await any([delay(10, 'x', true), delay(20, 'y')])); // y
try {
await any([delay(20, 'e1', true), delay(10, 'e2', true)]);
} catch (e) {
console.log(e instanceof AggregateError, e.errors); // true [ 'e1', 'e2' ]
}
console.log(await allSettled([])); // []
try {
await any([]);
} catch (e) {
console.log(e.errors.length); // 0
}
const winner = await Promise.race([race([]), delay(10, '空数组的 race 一直没结果')]);
console.log(winner); // 空数组的 race 一直没结果
}
main();
为什么 race 可以把 resolve 和 reject 直接传给每一项?因为一个 Promise 只能敲定一次。第一个有结果的项调用了 resolve 或 reject 以后,后面再调用都不起作用,所以不需要额外的标记。
为什么 allSettled 的结果里,10 毫秒就失败的“坏”排在第二位,而不是第一位?因为结果是按下标 results[i] 写进去的,不是按完成先后 push。MDN 说结果数组的顺序和输入顺序一致,any 的 errors 也一样,所以例子里 e2 先失败,errors 仍然是 ['e1', 'e2']。
count 和 index 比较,是因为输入是可迭代对象,不一定有 length,只能边遍历边计数。遍历是同步完成的,而回调最早也要到微任务里才执行,所以回调运行时 index 已经是总数了。
Promise.resolve(item) 让普通值也能参与。例子里的 3 不是 Promise,包装以后马上兑现,结果是 { status: 'fulfilled', value: 3 }。
空数组为什么各不相同
for 循环一次都不执行时,race 里没有任何地方调用 resolve 或 reject,返回的 Promise 就永远是 pending。这和 MDN 的描述一致:“An empty iterable causes the returned promise to be forever pending”。例子里用原生 Promise.race 给它配了一个 10 毫秒的对手,最后胜出的是对手。
allSettled 和 any 需要单独处理 index === 0。allSettled 没有要等的项,直接兑现为空数组;any 没有任何一项能成功,也就等于“全部失败”,MDN 写明这时返回的 Promise 已经是拒绝状态,原因是一个 errors 为空数组的 AggregateError。
这里有一个细节和原生不同。MDN 说 allSettled 和 any 只有在输入为空时才同步敲定,其他情况都是异步敲定;race 则总是异步敲定。上面的实现也符合这一点,因为非空时结果都在 then 回调里产生。
常见追问
怎样用 race 实现请求超时?
写一个
timeout(ms)返回在ms毫秒后拒绝的 Promise,和请求一起传给race。要注意race只决定用谁的结果,慢的请求不会被取消。真要中断请求,可以给fetch传AbortSignal.timeout(ms)或者自己的AbortController.signal。
allSettled 和 all 有什么区别?
all遇到第一个失败就立刻拒绝,适合“缺一不可”的场景;allSettled永远等到全部敲定,适合批量请求后分别处理成功和失败的场景。四个组合方法的选择可以看《Promise.all、allSettled、race 和 any 如何选择?》。
any 和 race 有什么区别?
race跟随第一个敲定的结果,第一个失败了它就失败;any会忽略失败,继续等下一个成功的,只有全部失败才拒绝。比如从多个镜像地址取同一份资源,用any能拿到最快的那个可用结果。