JavaScript 数组 .reduce 与 async/await

Posted

技术标签:

【中文标题】JavaScript 数组 .reduce 与 async/await【英文标题】:JavaScript array .reduce with async/await 【发布时间】:2017-05-05 17:41:35 【问题描述】:

似乎在将 async/await 与 .reduce() 结合时遇到了一些问题,如下所示:

const data = await bodies.reduce(async(accum, current, index) => 
  const methodName = methods[index]
  const method = this[methodName]
  if (methodName == 'foo') 
    current.cover = await this.store(current.cover, id)
    console.log(current)
    return 
      ...accum,
      ...current
    
  
  return 
    ...accum,
    ...method(current.data)
  
, )
console.log(data)

data 对象被记录之前this.store 完成...

我知道您可以将 Promise.all 与异步循环一起使用,但这是否适用于 .reduce()

【问题讨论】:

【参考方案1】:

问题在于您的累加器值是承诺 - 它们是 async functions 的返回值。要获得顺序评估(以及除了最后一次迭代之外的所有迭代),您需要使用

const data = await array.reduce(async (accumP, current, index) => 
  const accum = await accumP;
  …
, Promise.resolve(…));

也就是说,对于async/await,我一般会推荐给use plain loops instead of array iteration methods,它们的性能更高,而且通常更简单。

【讨论】:

最后感谢您的建议。我最终只使用了一个普通的 for 循环来完成我正在做的事情,它是相同的代码行,但更容易阅读...... reduce 中的initialValue 不必是Promise,但在大多数情况下它会阐明意图。 @EECOLOR 不过应该是这样。我真的不喜欢 await 必须将一个普通的值转换为一个承诺 @EECOLOR 并且在使用 TypeScript 时,初始值需要是一个 promise,因为回调的返回类型必须始终与累加器的类型匹配。 @jessedvrs 我认为您的意思是初始值(如果不是,我可能会误解您的意思)。你可以通过null 不是吗?【参考方案2】:

我喜欢Bergi的回答,我认为这是正确的方法。

我还想提一下我的一个库,名为Awaity.js

这让您可以毫不费力地使用reducemapfilterasync / await 等函数:

import reduce from 'awaity/reduce';

const posts = await reduce([1,2,3], async (posts, id) => 

  const res = await fetch('/api/posts/' + id);
  const post = await res.json();

  return 
    ...posts,
    [id]: post
  ;
, )

posts //  1:  ... , 2:  ... , 3:  ...  

【讨论】:

每遍都是连续的吗?还是批量调用所有这些 await 函数? 顺序的,因为每次迭代都依赖于前一次的返回值【参考方案3】:

[未解决 OP 的确切问题;专注于登陆这里的其他人。]

当您需要前面步骤的结果才能处理下一个步骤时,通常使用缩减。在这种情况下,您可以将 promise 串在一起:

promise = elts.reduce(
    async (promise, elt) => 
        return promise.then(async last => 
            return await f(last, elt)
        )
    , Promise.resolve(0)) // or "" or [] or ...

这是一个使用 fs.promise.mkdir() 的示例(当然,使用 mkdirSync 要简单得多,但就我而言,它是跨网络的):

const Path = require('path')
const Fs = require('fs')

async function mkdirs (path) 
    return path.split(/\//).filter(d => !!d).reduce(
        async (promise, dir) => 
            return promise.then(async parent => 
                const ret = Path.join(parent, dir);
                try 
                    await Fs.promises.lstat(ret)
                 catch (e) 
                    console.log(`mkdir($ret)`)
                    await Fs.promises.mkdir(ret)
                
                return ret
            )
        , Promise.resolve(""))


mkdirs('dir1/dir2/dir3')

下面是另一个添加 100 + 200 ... 500 并稍等片刻的示例:

async function slowCounter () 
    const ret = await ([100, 200, 300, 400, 500]).reduce(
        async (promise, wait, idx) => 
            return promise.then(async last => 
                const ret = last + wait
                console.log(`$idx: waiting $waitms to return $ret`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            )
        , Promise.resolve(0))
    console.log(ret)


slowCounter ()

【讨论】:

【参考方案4】:

有时最好的办法就是将两个代码版本并排放置,同步和异步:

同步版本:

const arr = [1, 2, 3, 4, 5];

const syncRev = arr.reduce((acc, i) => [i, ...acc], []); // [5, 4, 3, 2, 1] 

异步一:

(async () =>  
   const asyncRev = await arr.reduce(async (promisedAcc, i) => 
      const id = await asyncIdentity(i); // could be id = i, just stubbing async op.
      const acc = await promisedAcc;
      return [id, ...acc];
   , Promise.resolve([]));   // [5, 4, 3, 2, 1] 
)();

//async stuff
async function asyncIdentity(id) 
   return Promise.resolve(id);

const arr = [1, 2, 3, 4, 5];
(async () => 
    const asyncRev = await arr.reduce(async (promisedAcc, i) => 
        const id = await asyncIdentity(i);
        const acc = await promisedAcc;
        return [id, ...acc];
    , Promise.resolve([]));

    console.log('asyncRev :>> ', asyncRev);
)();

const syncRev = arr.reduce((acc, i) => [i, ...acc], []);

console.log('syncRev :>> ', syncRev);

async function asyncIdentity(id) 
    return Promise.resolve(id);

【讨论】:

这无法正确处理错误,请参阅***.com/questions/46889290/… 和***.com/questions/45285129/…。绝对不要使用这种模式! 如果你用 try catch 块包裹你的 reducer 主体,你绝对可以使用这种模式并正确处理错误,这样它总是能够返回累积值。【参考方案5】:

您可以将整个 map/reduce 迭代器块包装到它们自己的 Promise.resolve 中并等待其完成。但是,问题是累加器不包含您在每次迭代中期望的结果数据/对象。由于内部的 async/await/Promise 链,累加器将是实际的 Promise 本身,尽管在调用存储之前使用了 await 关键字,但可能尚未自行解决(这可能会让您相信迭代实际上不会返回,直到该调用完成并且累加器被更新。

虽然这不是最优雅的解决方案,但您可以选择将 data 对象变量移出范围并将其分配为 let 以便正确绑定并且可能发生突变。然后在 async/await/Promise 调用解析时从迭代器内部更新此数据对象。

/* allow the result object to be initialized outside of scope 
   rather than trying to spread results into your accumulator on iterations, 
   else your results will not be maintained as expected within the 
   internal async/await/Promise chain.
*/    
let data = ; 

await Promise.resolve(bodies.reduce(async(accum, current, index) => 
  const methodName = methods[index]
  const method = this[methodName];
  if (methodName == 'foo') 
    // note: this extra Promise.resolve may not be entirely necessary
    const cover = await Promise.resolve(this.store(current.cover, id));
    current.cover = cover;
    console.log(current);
    data = 
      ...data,
      ...current,
    ;
    return data;
  
  data = 
    ...data,
    ...method(current.data)
  ;
  return data;
, );
console.log(data);

【讨论】:

"累加器将是实际的 Promises 本身" - 是的,您的解决方案永远不会等待它们。它只等待从最后一次迭代返回的承诺,但如果解决的速度比以前的快,你的console.log(data) 将是不完整的。此解决方案不起作用。你应该只使用Promise.all【参考方案6】:

当前接受的答案建议使用Promise.all() 而不是async reduce。但是,这与 async reduce 的行为不同,并且仅适用于您希望异常立即停止所有迭代的情况,但情况并非总是如此。

此外,在该答案的 cmets 中,建议您始终将累加器作为减速器中的第一个语句等待,否则您可能会面临未处理的承诺拒绝的风险。海报还说这是 OP 所要求的,但事实并非如此。相反,他只想知道一切何时完成。为了知道你确实需要做await acc,但这可能在减速器的任何时候。

const reducer = async(acc, key) => 
  const response = await api(item);

  return 
    ...await acc, // <-- this would work just as well for OP
    [key]: reponse,
  

const result = await ['a', 'b', 'c', 'd'].reduce(reducer, );
console.log(result); // <-- Will be the final result

如何安全使用asyncreduce

话虽如此,以这种方式使用减速器确实意味着您需要保证它不会抛出,否则您将得到“未处理的承诺拒绝”。完全有可能通过使用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; //ms
function sleep(ms) 
    return new Promise(resolve => setTimeout(resolve, ms));


const api = async (key) => 
    console.log(`Calling API for $ key `);
    // Boz is a slow endpoint.
    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
        ;
    

    // OP doesn't care how this works, he only wants to know when the whole thing is ready.
    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); // <-- OP wants to execute things when it's ready.
)();

Promise.allSettled检查执行顺序:

const responseTime = 200; //ms
function sleep(ms) 
    return new Promise(resolve => setTimeout(resolve, ms));


const api = async (key) => 
    console.log(`Calling API for $ key `);
    // Boz is a slow endpoint.
    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); // <-- OP wants to execute things when it's ready.
)();

【讨论】:

【参考方案7】:

Bluebird 的另一个经典选项

const promise = require('bluebird');

promise.reduce([1,2,3], (agg, x) => Promise.resolve(agg+x),0).then(console.log);

// Expected to product sum 6

【讨论】:

【参考方案8】:

export const addMultiTextData = async(data) => 
  const textData = await data.reduce(async(a, 
    currentObject,
    selectedValue
  ) => 
    const 
      error,
      errorMessage
     = await validate(selectedValue, currentObject);
    return 
      ...await a,
      [currentObject.id]: 
        text: selectedValue,
        error,
        errorMessage
      
    ;
  , );
;

【讨论】:

虽然这段代码 sn-p 可以解决问题,但including an explanation 确实有助于提高帖子的质量。请记住,您是在为将来的读者回答问题,而这些人可能不知道您提出代码建议的原因。 并不是说我什至不会推荐这种方法,因为在循环中使用扩展运算符非常耗费性能。 这无法正确处理错误,请参阅***.com/questions/46889290/… 和***.com/questions/45285129/…。绝对不要使用这种模式!【参考方案9】:

以下是异步减少的方法:

async function asyncReduce(arr, fn, initialValue) 
  let temp = initialValue;

  for (let idx = 0; idx < arr.length; idx += 1) 
    const cur = arr[idx];

    temp = await fn(temp, cur, idx);
  

  return temp;

【讨论】:

以上是关于JavaScript 数组 .reduce 与 async/await的主要内容,如果未能解决你的问题,请参考以下文章

JavaScript归并方法reduce()和reduceRight()

javascript reduce map函数方法

Javascript中内建函数reduce的应用详解

javascript [数组数组的平均值]使用reduce()将每个值添加到累加器,使用值0初始化,除以a的长度

javascript reduce方法用的多么

JavaScript:使用 reduce() 和 map() 简化数组导致未定义