JavaScript 数组 .reduce with async/await

IT技术 javascript promise async-await reduce ecmascript-next
2021-01-21 23:12: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()?

6个回答

问题是您的累加器值是Promise - 它们是async functions 的返回值要获得顺序评估(以及所有等待的最后一次迭代),您需要使用

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

也就是说,对于async/await我一般会建议使用普通循环而不是数组迭代方法,它们的性能更高,而且通常更简单。

@EECOLOR 并且在使用 TypeScript 时,初始值需要是一个 promise,因为回调的返回类型必须始终匹配累加器的类型。
2021-03-17 23:12:35
@EECOLOR 不过应该是这样。我真的不喜欢await把一个普通的值变成一个Promise
2021-03-29 23:12:35
@jessedvrs 我认为你的意思是初始值(如果不是,我可能会误解你在说什么)。你能通过null不是吗?
2021-03-31 23:12:35
最后谢谢你的建议。我最终只使用了一个普通的 for 循环来完成我正在做的事情,它是相同的代码行,但更容易阅读......
2021-04-09 23:12:35
initialValuereduce并不需要一个Promise,它会然而,在大多数情况下,明确的意图。
2021-04-13 23:12:35

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

我还想提一下我的一个库,叫做Awaity.js

它可以让你毫不费力的使用功能,如reducemapfilter具有async / 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 函数?
2021-03-21 23:12:35
顺序的,因为每次迭代都依赖于前一次的返回值
2021-04-08 23:12:35

[不解决 OP 的确切问题;专注于降落在这里的其他人。]

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

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 ${wait}ms to return ${ret}`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            })
        }, Promise.resolve(0))
    console.log(ret)
}

slowCounter ()

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

同步版本:

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);
}

这无法正确处理错误,请参阅stackoverflow.com/questions/46889290/...stackoverflow.com/questions/45285129/...绝对不要使用这种模式!
2021-03-18 23:12:35
如果你用 try catch 块包裹你的减速器主体,你绝对可以使用这个模式并正确处理错误,这样它总是能够返回累积值。
2021-03-18 23:12:35

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

虽然这不是最优雅的解决方案,但您可以选择的一种方法是将数据对象变量移出作用域并将其分配为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);
累加器本身就是实际的 Promise ” - 是的,您的解决方案永远不会等待它们。它只等待从最后一次迭代返回的Promise,但如果它比以前的更快解决,你的console.log(data)将是不完整的。此解决方案不起作用。你应该只使用Promise.all.
2021-03-24 23:12:35