在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的代码更少,这是它如何帮助我从数据库中解析数据,从几个不同的子引用中,然后将它们全部推到一个数组中,并在完成所有任务后以承诺的方式进行解析:

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)))

其他回答

这里是一个在forEach循环中使用异步的好例子。

编写自己的asyncForEach

async function asyncForEach(array, callback) {  
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array)
    }
}

你可以这样用

await asyncForEach(array, async function(item,index,array){
     //await here
   }
)

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

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

除了@Bergi的回答,我想提供第三种选择。这与@Bergi的第二个例子非常相似,但不是单独等待每个readFile,而是创建一个承诺数组,每个承诺都在最后等待。

import fs from 'fs-promise';
async function printFiles () {
  const files = await getFilePaths();

  const promises = files.map((file) => fs.readFile(file, 'utf8'))

  const contents = await Promise.all(promises)

  contents.forEach(console.log);
}

注意,传递给.map()的函数不需要是异步的,因为fs.readFile无论如何都会返回Promise对象。因此,Promise是Promise对象的数组,可以将其发送到Promise.all()。

在@Bergi的回答中,控制台可以按照文件内容的读取顺序记录文件内容。例如,如果一个非常小的文件在一个非常大的文件之前完成了读取,那么它将首先被记录,即使小文件在文件数组中位于大文件之后。然而,在我上面的方法中,您可以保证控制台将以与提供的阵列相同的顺序记录文件。