JavaScript数组.reduce与async / await


78

似乎在将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()

Answers:


134

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

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

就是说,对于async/await我通常建议使用纯循环而不是数组迭代方法,它们更高效且通常更简单。


3
最后感谢您的建议。我最终只是对自己的操作使用普通的for循环,并且代码行相同,但更容易阅读……
cs_pupil

3
initialValuereduce并不需要一个Promise,它会然而,在大多数情况下,明确的意图。
EECOLOR

@EECOLOR应该是。我真的不喜欢await将简单的价值转化为诺言
Bergi

1
@EECOLOR并且在使用TypeScript时,初始值必须是一个promise,因为回调的返回类型必须始终与累加器的类型匹配。
jessedvrs

@jessedvrs我认为您的意思是初始值(如果不是,我可能会误解您的意思)。你可以通过null吗?
EECOLOR

5

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

我还想提到我的一个名为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函数?
wle8300

1
顺序的,因为每次迭代都取决于上一个迭代的返回值
Asaf Katz,

2

您可以将整个map / reduce迭代器块包装到自己的Promise.resolve中,然后等待完成。但是,问题在于累加器不包含您在每次迭代中期望的结果数据/对象。由于内部有async / await / Promise链,所以累加器将是实际的Promises本身,尽管在调用存储之前使用了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);

0

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


3
尽管此代码段可以解决问题,但提供说明确实有助于提高您的帖子质量。请记住,您将来会为读者回答这个问题,而这些人可能不知道您提出代码建议的原因。
Shree

更不用说我什至不推荐这种方法,因为在循环中使用扩展运算符会增加性能。
罗伯特·莫利纳

0

[未解决OP的确切概率;专注于落在这里的其他人。]

当您需要上一步的结果才能处理下一个步骤时,通常使用Reduce。在这种情况下,您可以将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 ${wait}ms to return ${ret}`)
                await new Promise((res, rej) => setTimeout(res, wait))
                return ret
            })
        }, Promise.resolve(0))
    console.log(ret)
}

slowCounter ()


0

这是使异步减少的方法:

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;
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.