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


当前回答

与Antonio Val的p迭代类似,另一种npm模块是异步af:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  // since AsyncAF accepts promises or non-promises, there's no need to await here
  const files = getFilePaths();

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

printFiles();

或者,async-af有一个静态方法(log/logAF)来记录promise的结果:

const AsyncAF = require('async-af');
const fs = require('fs-promise');

function printFiles() {
  const files = getFilePaths();

  AsyncAF(files).forEach(file => {
    AsyncAF.log(fs.readFile(file, 'utf8'));
  });
}

printFiles();

然而,该库的主要优点是您可以链接异步方法来执行以下操作:

const aaf = require('async-af');
const fs = require('fs-promise');

const printFiles = () => aaf(getFilePaths())
  .map(file => fs.readFile(file, 'utf8'))
  .forEach(file => aaf.log(file));

printFiles();

异步af

其他回答

这不会像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)
})

就像@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容易出错,并将错误与应用程序分开处理。

然而,上述两种解决方案都有效,Antonio的代码更少,这是它如何帮助我从数据库中解析数据,从几个不同的子引用中,然后将它们全部推到一个数组中,并在完成所有任务后以承诺的方式进行解析:

Promise.all(PacksList.map((pack)=>{
    return fireBaseRef.child(pack.folderPath).once('value',(snap)=>{
        snap.forEach( childSnap => {
            const file = childSnap.val()
            file.id = childSnap.key;
            allItems.push( file )
        })
    })
})).then(()=>store.dispatch( actions.allMockupItems(allItems)))

一个重要的警告是:等待+等待。。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);
    }
}

目前,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>之前创建的任何数组都不具有此功能