当前被接受的答案建议使用
Promise.all()
而不是
async
reduce
。然而,这与
async
reduce
的行为不同,仅适用于您希望异常立即停止所有迭代的情况,这并不总是适用的。
此外,在该答案的评论中建议始终将累加器作为reducer中的第一条语句进行等待,否则可能会出现未处理的promise拒绝风险。发帖者还说这就是OP所要求的,但事实并非如此。相反,他只想知道何时完成所有操作。为了确切知道这一点,您确实需要在reducer的任何时间点上做
await acc
。
const reducer = async(acc, key) => {
const response = await api(item);
return {
...await acc,
[key]: response,
}
}
const result = await ['a', 'b', 'c', 'd'].reduce(reducer, {});
console.log(result);
如何安全地使用async
reduce
尽管使用reducer这种方式很方便,但是你必须保证它不会抛出异常,否则你将会得到"未处理的promise拒绝"。可以使用try-catch
来保证这一点,如果出现异常,catch
块应该返回累加器(可以附带一个记录失败的API调用的记录)。
const reducer = async (acc, key) => {
try {
data = await doSlowTask(key);
return {...await acc, [key]: data};
} catch (error) {
return {...await acc, [key]: {error}};
};
}
const result = await ['a', 'b', 'c','d'].reduce(reducer, {});
Promise.allSettled
与之间的区别
您可以使用Promise.allSettled
接近具有错误捕获功能的async
reduce
行为。但是,这种使用方式很笨拙:如果要缩小到对象,则需要在其后添加另一个同步缩小。
理论时间复杂度对于Promise.allSettled
+常规reduce
也更高,尽管可能有非常少的用例会产生影响。 async
reduce
可以从第一个项目完成时开始累加,而在Promise.allSettled
之后的reduce
被阻塞,直到所有承诺都已实现。当循环遍历大量元素时,这可能会产生影响。
const responseTime = 200;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const api = async (key) => {
console.log(`Calling API for ${ key }`);
await sleep(key === 'boz' ? 800 : responseTime);
console.log(`Got response for ${ key }`);
if (key === 'bar') throw new Error(`It doesn't work for ${ key }`);
return {
[key]: `API says ${ key }`,
};
};
const keys = ['foo', 'bar', 'baz', 'buz', 'boz'];
const reducer = async (acc, key) => {
let data;
try {
const response = await api(key);
data = {
apiData: response
};
} catch (e) {
data = {
error: e.message
};
}
const previous = await acc;
console.log(`Got previous for ${ key }`);
return {
...previous,
[key]: {
...data
},
};
};
(async () => {
const start = performance.now();
const result = await keys.reduce(reducer, {});
console.log(`After ${ performance.now() - start }ms`, result);
})();
使用Promise.allSettled
检查执行顺序:
const responseTime = 200;
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
const api = async (key) => {
console.log(`Calling API for ${ key }`);
await sleep(key === 'boz' ? 800 : responseTime);
console.log(`Got response for ${ key }`);
if (key === 'bar') throw new Error(`It doesn't work for ${ key }`);
return {
key,
data: `API says ${ key }`,
};
};
const keys = ['foo', 'bar', 'baz', 'buz', 'boz'];
(async () => {
const start = performance.now();
const apiResponses = await Promise.allSettled(keys.map(api));
const result = apiResponses.reduce((acc, {status, reason, value}) => {
const {key, data} = value || {};
console.log(`Got previous for ${ key }`);
return {
...acc,
[key]: status === 'fulfilled' ? {apiData: data} : {error: reason.message},
};
}, {});
console.log(`After ${ performance.now() - start }ms`, result);
})();
reduce
的initialValue
不需要是一个Promise
,但在大多数情况下,使用Promise
可以更清晰地表达意图。 - EECOLORreduce
中,你可以将整个调用包装在try
/catch
中,异常会按预期传播。你不需要单独包装每个可能抛出异常的代码部分 - 这也可能是处理它们的错误位置不正确。但是对于异步的 reducer,只有在你正确(立即)await
处理的每个 promise - 包括累加器 promise 时,编写try { await array.reduce(…) } catch …
才能正常工作。 - Bergi