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


当前回答

就像@Bergi的回应,但有一点不同。

承诺。如果一个人被拒绝,所有人都会拒绝所有承诺。

所以,使用递归。

const readFilesQueue = async (files, index = 0) {
    const contents = await fs.readFile(files[index], 'utf8')
    console.log(contents)

    return files.length <= index
        ? readFilesQueue(files, ++index)
        : files

}

const printFiles async = () => {
    const files = await getFilePaths();
    const printContents = await readFilesQueue(files)

    return printContents
}

printFiles()

PS

readFilesQueue在printFiles之外。由于console.log引入了副作用*,所以最好是模拟、测试或监视,因此,使用返回内容的函数(sidenuote)并不酷。

因此,代码可以简单地这样设计:三个独立的函数是“纯”**,不会产生任何副作用,处理整个列表,并且可以很容易地修改以处理失败的案例。

const files = await getFilesPath()

const printFile = async (file) => {
    const content = await fs.readFile(file, 'utf8')
    console.log(content)
}

const readFiles = async = (files, index = 0) => {
    await printFile(files[index])

    return files.lengh <= index
        ? readFiles(files, ++index)
        : files
}

readFiles(files)

未来编辑/当前状态

节点支持顶级等待(它还没有插件,也不会有,可以通过和谐标志启用),这很酷,但不能解决一个问题(策略上我只在LTS版本上工作)。如何获取文件?

使用合成。给出代码后,我觉得这是在一个模块内,所以应该有一个函数来完成。如果没有,你应该使用IIFE将角色代码包装成一个异步函数,创建一个简单的模块,它可以为你做所有的事情,或者你可以采用正确的方式,即组合。

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

注意,变量的名称因语义而改变。您传递一个函子(一个可以被另一个函数调用的函数),并在内存中接收一个指针,该指针包含应用程序的初始逻辑块。

但是,如果不是模块,您需要导出逻辑?

将函数包装在异步函数中。

export const readFilesQueue = async () => {
    // ... to code goes here
}

或者改变变量的名称。。。


*副作用是指应用程序的任何协同作用,它可以改变状态/行为或在应用程序中引入错误,如IO。

**通过“纯”,它是撇号,因为函数不是纯的,当没有控制台输出,只有数据操作时,代码可以聚合为纯版本。

除此之外,为了纯粹起见,您需要使用处理副作用的monad,这些monad容易出错,并将错误与应用程序分开处理。

其他回答

下面是一些forEachAsync原型。注意,您需要等待他们:

Array.prototype.forEachAsync = async function (fn) {
    for (let t of this) { await fn(t) }
}

Array.prototype.forEachAsyncParallel = async function (fn) {
    await Promise.all(this.map(fn));
}

请注意,虽然您可以将其包含在自己的代码中,但不应将其包含到您分发给其他人的库中(以避免污染其全局)。

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

当fs基于承诺时,Bergi的解决方案非常有效。您可以使用bluebird、fs extra或fs promise。

然而,节点的本地fs库的解决方案如下:

const result = await Promise.all(filePaths
    .map( async filePath => {
      const fileContents = await getAssetFromCache(filePath, async function() {

        // 1. Wrap with Promise    
        // 2. Return the result of the Promise
        return await new Promise((res, rej) => {
          fs.readFile(filePath, 'utf8', function(err, data) {
            if (data) {
              res(data);
            }
          });
        });
      });

      return fileContents;
    }));

注:require('fs')强制将函数作为第三个参数,否则抛出错误:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function

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

例如:

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

to

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

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

就像@Bergi的回应,但有一点不同。

承诺。如果一个人被拒绝,所有人都会拒绝所有承诺。

所以,使用递归。

const readFilesQueue = async (files, index = 0) {
    const contents = await fs.readFile(files[index], 'utf8')
    console.log(contents)

    return files.length <= index
        ? readFilesQueue(files, ++index)
        : files

}

const printFiles async = () => {
    const files = await getFilePaths();
    const printContents = await readFilesQueue(files)

    return printContents
}

printFiles()

PS

readFilesQueue在printFiles之外。由于console.log引入了副作用*,所以最好是模拟、测试或监视,因此,使用返回内容的函数(sidenuote)并不酷。

因此,代码可以简单地这样设计:三个独立的函数是“纯”**,不会产生任何副作用,处理整个列表,并且可以很容易地修改以处理失败的案例。

const files = await getFilesPath()

const printFile = async (file) => {
    const content = await fs.readFile(file, 'utf8')
    console.log(content)
}

const readFiles = async = (files, index = 0) => {
    await printFile(files[index])

    return files.lengh <= index
        ? readFiles(files, ++index)
        : files
}

readFiles(files)

未来编辑/当前状态

节点支持顶级等待(它还没有插件,也不会有,可以通过和谐标志启用),这很酷,但不能解决一个问题(策略上我只在LTS版本上工作)。如何获取文件?

使用合成。给出代码后,我觉得这是在一个模块内,所以应该有一个函数来完成。如果没有,你应该使用IIFE将角色代码包装成一个异步函数,创建一个简单的模块,它可以为你做所有的事情,或者你可以采用正确的方式,即组合。

// more complex version with IIFE to a single module
(async (files) => readFiles(await files())(getFilesPath)

注意,变量的名称因语义而改变。您传递一个函子(一个可以被另一个函数调用的函数),并在内存中接收一个指针,该指针包含应用程序的初始逻辑块。

但是,如果不是模块,您需要导出逻辑?

将函数包装在异步函数中。

export const readFilesQueue = async () => {
    // ... to code goes here
}

或者改变变量的名称。。。


*副作用是指应用程序的任何协同作用,它可以改变状态/行为或在应用程序中引入错误,如IO。

**通过“纯”,它是撇号,因为函数不是纯的,当没有控制台输出,只有数据操作时,代码可以聚合为纯版本。

除此之外,为了纯粹起见,您需要使用处理副作用的monad,这些monad容易出错,并将错误与应用程序分开处理。