在forEach循环中使用async/await有什么问题吗?我正在尝试循环浏览一系列文件,并等待每个文件的内容。

import fs from 'fs-promise'

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
  })
}

printFiles()

这段代码确实有效,但这段代码会出错吗?我有人告诉我,你不应该在这样的高阶函数中使用async/await,所以我只想问问这是否有问题。


当前回答

对于TypeScript用户,使用工作类型的Promise.all(array.map(迭代器))包装器

使用Promise.all(array.map(迭代器))具有正确的类型,因为TypeScript的stdlib支持已经处理了泛型。然而,每次需要异步映射时复制粘贴Promise.all(array.map(迭代器))显然不是最佳的,Promise.all(array.ma(迭代))并不能很好地传达代码的意图,因此大多数开发人员都会将其包装成一个asyncMap()包装函数。然而,要做到这一点,需要使用泛型来确保使用const value=await asyncMap()设置的值具有正确的类型。

export const asyncMap = async <ArrayItemType, IteratorReturnType>(
  array: Array<ArrayItemType>,
  iterator: (
    value: ArrayItemType,
    index?: number
  ) => Promise<IteratorReturnType>
): Promise<Array<IteratorReturnType>> => {
  return Promise.all(array.map(iterator));
};

快速测试:

it(`runs 3 items in parallel and returns results`, async () => {
  const result = await asyncMap([1, 2, 3], async (item: number) => {
    await sleep(item * 100);
    return `Finished ${item}`;
  });
  expect(result.length).toEqual(3);
  // Each item takes 100, 200 and 300ms
  // So restricting this test to 300ms plus some leeway
}, 320);

sleep()只是:

const sleep = async (timeInMs: number): Promise<void> => {
  return new Promise((resolve) => setTimeout(resolve, timeInMs));
};

其他回答

使用Task、futurize和可遍历列表,您可以简单地

async function printFiles() {
  const files = await getFiles();

  List(files).traverse( Task.of, f => readFile( f, 'utf-8'))
    .fork( console.error, console.log)
}

这是你如何设置的

import fs from 'fs';
import { futurize } from 'futurize';
import Task from 'data.task';
import { List } from 'immutable-ext';

const future = futurizeP(Task)
const readFile = future(fs.readFile)

构建所需代码的另一种方法是

const printFiles = files => 
  List(files).traverse( Task.of, fn => readFile( fn, 'utf-8'))
    .fork( console.error, console.log)

或者甚至更注重功能

// 90% of encodings are utf-8, making that use case super easy is prudent

// handy-library.js
export const readFile = f =>
  future(fs.readFile)( f, 'utf-8' )

export const arrayToTaskList = list => taskFn => 
  List(files).traverse( Task.of, taskFn ) 

export const readFiles = files =>
  arrayToTaskList( files, readFile )

export const printFiles = files => 
  readFiles(files).fork( console.error, console.log)

然后从父函数

async function main() {
  /* awesome code with side-effects before */
  printFiles( await getFiles() );
  /* awesome code with side-effects after */
}

如果你真的想在编码上有更多的灵活性,你可以这样做(为了好玩,我使用了建议的Pipe Forward操作符)

import { curry, flip } from 'ramda'

export const readFile = fs.readFile 
  |> future,
  |> curry,
  |> flip

export const readFileUtf8 = readFile('utf-8')

PS-我没有在控制台上尝试这段代码,可能有一些拼写错误。。。正如90年代的孩子们所说的那样,“直式自由泳,从穹顶上跳下来!”-p

您可以使用async包中的async.forEach循环:

async.forEach(dataToLoop(array), async(data, cb) => {
                variable = await MongoQuery;
            }, function(err) {
                console.log(err);  
              })
            })
            .catch((err)=>{
              console.log(err);
            })

对于TypeScript用户,使用工作类型的Promise.all(array.map(迭代器))包装器

使用Promise.all(array.map(迭代器))具有正确的类型,因为TypeScript的stdlib支持已经处理了泛型。然而,每次需要异步映射时复制粘贴Promise.all(array.map(迭代器))显然不是最佳的,Promise.all(array.ma(迭代))并不能很好地传达代码的意图,因此大多数开发人员都会将其包装成一个asyncMap()包装函数。然而,要做到这一点,需要使用泛型来确保使用const value=await asyncMap()设置的值具有正确的类型。

export const asyncMap = async <ArrayItemType, IteratorReturnType>(
  array: Array<ArrayItemType>,
  iterator: (
    value: ArrayItemType,
    index?: number
  ) => Promise<IteratorReturnType>
): Promise<Array<IteratorReturnType>> => {
  return Promise.all(array.map(iterator));
};

快速测试:

it(`runs 3 items in parallel and returns results`, async () => {
  const result = await asyncMap([1, 2, 3], async (item: number) => {
    await sleep(item * 100);
    return `Finished ${item}`;
  });
  expect(result.length).toEqual(3);
  // Each item takes 100, 200 and 300ms
  // So restricting this test to 300ms plus some leeway
}, 320);

sleep()只是:

const sleep = async (timeInMs: number): Promise<void> => {
  return new Promise((resolve) => setTimeout(resolve, timeInMs));
};

一个重要的警告是:等待+等待。。of方法和forEach+异步方式实际上有不同的效果。

在真正的for循环中等待将确保所有异步调用都逐一执行。forEach+异步方式将同时发出所有的承诺,这会更快,但有时会不堪重负(如果您执行一些DB查询或访问一些具有容量限制的web服务,并且不想一次发出100000个呼叫)。

如果您不使用async/await,并且希望确保一个接一个地读取文件,那么也可以使用reduce+promise(不太优雅)。

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

或者您可以创建一个forEachAsync来帮助,但基本上使用相同的for循环底层。

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}

files.forEach(异步(文件)=>{const contents=await fs.readFile(文件,'utf8')})

问题是,forEach()忽略了迭代函数返回的promise。在每个异步代码执行完成后,forEach不会等待移动到下一个迭代。所有fs.readFile函数将在同一轮事件循环中调用,这意味着它们是并行启动的,而不是顺序启动的,并且在调用forEach()后立即继续执行等待所有fs.readFile操作完成。由于forEach不等待每个promise解析,因此循环实际上在解析promise之前完成了迭代。您希望在forEach完成后,所有异步代码都已执行,但事实并非如此。您最终可能会尝试访问尚未可用的值。

您可以使用以下示例代码测试行为

常量数组=[1,2,3];const sleep=(ms)=>new Promise(解析=>setTimeout(解析,ms));常量delayedSquare=(num)=>睡眠(100)。然后(()=>num*num);const testForEach=(numbersArray)=>{常量存储=[];//此处将此代码视为同步代码numbersArray.forEach(异步(num)=>{const squaredNum=等待延迟平方(num);//这将控制相应的squaredNum值console.log(平方数);store.push(平方数);});//您希望存储阵列已填充,但未填充//这将返回[]console.log(“存储”,存储);};testForEach(数组);//注意,当您测试时,将记录第一个“store[]”//然后squaredNum的内部forEach将记录

解决方案是使用for of循环。

for (const file of files){
    const contents = await fs.readFile(file, 'utf8')
}