在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,所以我只想问问这是否有问题。


当前回答

正如其他答案所提到的,您可能希望它按顺序而不是并行执行。即,运行第一个文件,等待完成,然后一旦完成,运行第二个文件。这不会发生。

我认为很重要的是要解决为什么没有发生这种情况。

想想forEach是如何工作的。我找不到来源,但我认为它的工作原理如下:

const forEach = (arr, cb) => {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i]);
  }
};

现在想想当你做这样的事情时会发生什么:

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

在forEach的for循环中,我们调用cb(arr[i]),最后是logFile(file)。logFile函数内部有一个await,所以for循环可能会在继续到i++之前等待这个await?

不,不会的。令人困惑的是,这不是wait的工作方式。从文档中:

await分割执行流,允许异步函数的调用方继续执行。在await延迟异步函数的继续之后,随后执行后续语句。如果此await是其函数执行的最后一个表达式,则继续执行,方法是向函数的调用方返回完成await函数的未决Promise并继续执行该调用方。

因此,如果您有以下内容,则不会在“b”之前记录数字:

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

const logNumbers = async () => {
  console.log(1);
  await delay(2000);
  console.log(2);
  await delay(2000);
  console.log(3);
};

const main = () => {
  console.log("a");
  logNumbers();
  console.log("b");
};

main();

循环回到forEach,forEach就像main,logFile就像logNumbers。main不会因为logNumbers等待而停止,forEach不会因为logFile等待而停止。

其他回答

使用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

目前,Array.forEach原型属性不支持异步操作,但我们可以创建自己的多边形填充来满足我们的需要。

// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function 
async function asyncForEach(iteratorFunction){
  let indexer = 0
  for(let data of this){
    await iteratorFunction(data, indexer)
    indexer++
  }
}
// Append it as an Array prototype property
Array.prototype.asyncForEach = asyncForEach
module.exports = {Array}

就这样!现在,在这些操作之后定义的任何数组上都可以使用asyncforEach方法。

让我们测试一下。。。

// Nodejs style
// file: someOtherFile.js

const readline = require('readline')
Array = require('./asyncForEach').Array
const log = console.log

// Create a stream interface
function createReader(options={prompt: '>'}){
  return readline.createInterface({
    input: process.stdin
    ,output: process.stdout
    ,prompt: options.prompt !== undefined ? options.prompt : '>'
  })
}
// Create a cli stream reader
async function getUserIn(question, options={prompt:'>'}){
  log(question)
  let reader = createReader(options)
  return new Promise((res)=>{
    reader.on('line', (answer)=>{
      process.stdout.cursorTo(0, 0)
      process.stdout.clearScreenDown()
      reader.close()
      res(answer)
    })
  })
}

let questions = [
  `What's your name`
  ,`What's your favorite programming language`
  ,`What's your favorite async function`
]
let responses = {}

async function getResponses(){
// Notice we have to prepend await before calling the async Array function
// in order for it to function as expected
  await questions.asyncForEach(async function(question, index){
    let answer = await getUserIn(question)
    responses[question] = answer
  })
}

async function main(){
  await getResponses()
  log(responses)
}
main()
// Should prompt user for an answer to each question and then 
// log each question and answer as an object to the terminal

我们可以对其他一些数组函数(如map。。。

async function asyncMap(iteratorFunction){
  let newMap = []
  let indexer = 0
  for(let data of this){
    newMap[indexer] = await iteratorFunction(data, indexer, this)
    indexer++
  }
  return newMap
}

Array.prototype.asyncMap = asyncMap

…等等:)

需要注意的一些事项:

迭代器函数必须是异步函数或promise在Array.protocol.<yourAsyncFunc>=<yourAsync Func>之前创建的任何数组都不具有此功能

替换forEach()等待循环的一个简单的解决方案是用map替换forEach,并在开头添加Promise.all()。

例如:

await y.forEach(异步(x)=>{

to

await Promise.all(y.map(异步(x)=>{

结尾处需要一个额外的)。

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

这不会像OP请求的那样使用async/await,只有当您在NodeJS的后端时才有效。尽管这对某些人来说可能还是有帮助的,因为OP给出的示例是读取文件内容,通常在后端进行文件读取。

完全异步和非阻塞:

const fs = require("fs")
const async = require("async")

const obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}
const configs = {}

async.forEachOf(obj, (value, key, callback) => {
    fs.readFile(__dirname + value, "utf8", (err, data) => {
        if (err) return callback(err)
        try {
            configs[key] = JSON.parse(data);
        } catch (e) {
            return callback(e)
        }
        callback()
    });
}, err => {
    if (err) console.error(err.message)
    // configs is now a map of JSON data
    doSomethingWith(configs)
})