繁体   English   中英

将 async/await 与 forEach 循环一起使用

[英]Using async/await with a forEach loop

forEach循环中使用async / await有什么问题吗? 我正在尝试遍历一组文件并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 ,所以我只想问问这是否有任何问题。

当然代码确实可以工作,但我很确定它没有按照您的预期做。 它只是触发多个异步调用,但printFiles函数会在此之后立即返回。

按顺序阅读

如果要按顺序读取文件,则确实不能使用forEach 只需使用现代for … of循环,其中await将按预期工作:

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

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

并行阅读

如果您想并行读取文件,则确实不能使用forEach 每个async回调函数调用都会返回一个 Promise,但您将它们扔掉而不是等待它们。 只需使用map代替,您就可以等待通过Promise.all获得的一系列承诺:

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

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

使用 ES2018,您可以大大简化上述所有答案:

async function printFiles () {
  const files = await getFilePaths()

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

参见规范: proposal-async-iteration

简化:

  for await (const results of array) {
    await longRunningTask()
  }
  console.log('I will wait')

2018-09-10:这个答案最近引起了很多关注,有关异步迭代的更多信息,请参阅Axel Rauschmayer 的博客文章

代替Promise.allArray.prototype.map (它不保证Promise的解决顺序),我使用Array.prototype.reduce ,从解决的Promise开始:

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

  await files.reduce(async (promise, file) => {
    // This line will wait for the last async function to finish.
    // The first iteration uses an already resolved Promise
    // so, it will immediately continue.
    await promise;
    const contents = await fs.readFile(file, 'utf8');
    console.log(contents);
  }, Promise.resolve());
}

npm 上的p-iteration模块实现了 Array 迭代方法,因此它们可以以非常直接的方式与 async/await 一起使用。

以您的情况为例:

const { forEach } = require('p-iteration');
const fs = require('fs-promise');

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

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

这里有一些forEachAsync原型。 请注意,您需要await他们:

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

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

价值 1000 字的图片 - 仅适用于顺序方法


背景:我昨晚也遇到了类似的情况。 我使用 async 函数作为 foreach 参数。 结果是无法预料的。 当我对我的代码进行 3 次测试时,它运行了 2 次没有问题并且失败了 1 次。 (有点奇怪)

最后我明白了,做了一些便笺簿测试。

场景 1 - 在 foreach 中使用 async 可以获得多么不连续

在此处输入图像描述

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  myPromiseArray.forEach(async (element, index) => {
    let result = await element;
    console.log(result);
  })

  console.log('After For Each Loop')
}

main();

场景 2 - 使用for - of循环,如上面建议的@Bergi

在此处输入图像描述

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well
  for (const element of myPromiseArray) {
    let result = await element;
    console.log(result)
  }

  console.log('After For Each Loop')
}

main();

如果你像我一样是老派,你可以简单地使用经典的 for 循环,它也可以:)

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well too - the classic for loop :)
  for (let i = 0; i < myPromiseArray.length; i++) {
    const result = await myPromiseArray[i];
    console.log(result);
  }

  console.log('After For Each Loop')
}

main();

我希望这对某人有帮助,美好的一天,干杯!

@Bergi 已经给出了如何正确处理这种特殊情况的答案。 我不会在这里复制。

我想解决在asyncawait方面使用forEachfor循环之间的区别

forEach工作原理

让我们看看forEach是如何工作的。 根据ECMAScript 规范,MDN 提供了一个可以用作 polyfill 的实现 我将其复制并粘贴到此处并删除评论。

Array.prototype.forEach = function (callback, thisArg) {
  if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); }
  var T, k;
  var O = Object(this);
  var len = O.length >>> 0;
  if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); }
  if (arguments.length > 1) { T = thisArg; }
  k = 0;
  while (k < len) {
    var kValue;
    if (k in O) {
      kValue = O[k];
      callback.call(T, kValue, k, O); // pay attention to this line
    }
    k++;
  }
};

让我们回到您的代码,让我们将回调提取为一个函数。

async function callback(file){
  const contents = await fs.readFile(file, 'utf8')
  console.log(contents)
}

所以,基本上callback返回一个承诺,因为它是用async声明的。 forEach内部, callback只是以正常方式调用,如果回调本身返回一个 Promise,则 javascript 引擎不会等待它被解析或拒绝。 相反,它将promise放入作业队列,并继续执行循环。

callback await fs.readFile(file, 'utf8')怎么样?

基本上,当您的异步callback有机会执行时,js 引擎将暂停,直到fs.readFile(file, 'utf8')被解析或拒绝,并在完成后恢复异步函数的执行。 所以contents变量存储来自fs.readFile的实际结果,而不是promise 因此, console.log(contents)注销文件内容而不是Promise

为什么for ... of作品?

当我们编写一个通用for of循环时,我们获得了比forEach更多的控制权。 让我们重构printFiles

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

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
    // or await callback(file)
  }
}

当评估for循环时,我们在async函数中有await Promise,执行将暂停,直到await Promise 完成。 因此,您可以认为文件是按照确定的顺序逐一读取的。

依次执行

有时,我们确实需要按顺序执行异步函数。 例如,我有一些存储在数组中的新记录要保存到数据库中,我希望它们按顺序保存,这意味着数组中的第一条记录应该首先保存,然后是第二条,直到最后一条被保存。

这是一个例子:

 const records = [1, 2, 3, 4]; async function saveRecord(record) { return new Promise((resolved, rejected) => { setTimeout(()=> { resolved(`record ${record} saved`) }, Math.random() * 500) }); } async function forEachSaveRecords(records) { records.forEach(async (record) => { const res = await saveRecord(record); console.log(res); }) } async function forofSaveRecords(records) { for (const record of records) { const res = await saveRecord(record); console.log(res); } } (async () => { console.log("=== for of save records ===") await forofSaveRecords(records) console.log("=== forEach save records ===") await forEachSaveRecords(records) })()

我使用setTimeout来模拟将记录保存到数据库的过程 - 它是异步的并且花费随机时间。 使用forEach ,记录以未确定的顺序保存,但使用for..of ,它们按顺序保存。

该解决方案还针对内存进行了优化,因此您可以在 10,000 个数据项和请求上运行它。 这里的一些其他解决方案会使服务器在大型数据集上崩溃。

在打字稿中:

export async function asyncForEach<T>(array: Array<T>, callback: (item: T, index: number) => Promise<void>) {
        for (let index = 0; index < array.length; index++) {
            await callback(array[index], index);
        }
    }

如何使用?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
files.forEach(async (file) => { const contents = await fs.readFile(file, 'utf8') })

问题是,迭代函数返回的承诺被forEach()忽略了。 结果,所有的fs.readFile函数都在同一轮事件循环中被调用,这意味着它们是并行启动的,而不是按顺序启动的,并且在调用 forEach() 后立即继续执行,而无需等待所有fs.readFile操作完成。 由于 forEach 不等待每个 Promise 解决,因此循环实际上在 Promise 解决之前完成迭代。 您最终可能会尝试访问尚不可用的值。

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

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()的函数不需要是async ,因为fs.readFile无论如何都会返回一个 Promise 对象。 因此, promises是一个 Promise 对象数组,可以发送到Promise.all()

在@Bergi 的回答中,控制台可能会按照读取的顺序记录文件内容。 例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使files数组中小文件大文件之后也是如此。 但是,在我上面的方法中,您可以保证控制台将以与提供的数组相同的顺序记录文件。

替换不起作用的forEach()等待循环的简单直接解决方案是将forEach替换为map并将Promise.all(添加到开头。

例如:

await y.forEach(async (x) => {

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

最后需要一个额外的)

在一个文件中弹出几个方法非常容易,这些方法将按序列化顺序处理异步数据并为您的代码提供更传统的风格。 例如:

module.exports = function () {
  var self = this;

  this.each = async (items, fn) => {
    if (items && items.length) {
      await Promise.all(
        items.map(async (item) => {
          await fn(item);
        }));
    }
  };

  this.reduce = async (items, fn, initialValue) => {
    await self.each(
      items, async (item) => {
        initialValue = await fn(initialValue, item);
      });
    return initialValue;
  };
};

现在,假设它保存在“./myAsync.js”中,您可以在相邻文件中执行类似于以下内容的操作:

...
/* your server setup here */
...
var MyAsync = require('./myAsync');
var Cat = require('./models/Cat');
var Doje = require('./models/Doje');
var example = async () => {
  var myAsync = new MyAsync();
  var doje = await Doje.findOne({ name: 'Doje', noises: [] }).save();
  var cleanParams = [];

  // FOR EACH EXAMPLE
  await myAsync.each(['bork', 'concern', 'heck'], 
    async (elem) => {
      if (elem !== 'heck') {
        await doje.update({ $push: { 'noises': elem }});
      }
    });

  var cat = await Cat.findOne({ name: 'Nyan' });

  // REDUCE EXAMPLE
  var friendsOfNyanCat = await myAsync.reduce(cat.friends,
    async (catArray, friendId) => {
      var friend = await Friend.findById(friendId);
      if (friend.name !== 'Long cat') {
        catArray.push(friend.name);
      }
    }, []);
  // Assuming Long Cat was a friend of Nyan Cat...
  assert(friendsOfNyanCat.length === (cat.friends.length - 1));
}

fs基于 promise 时, Bergi 的解决方案效果很好。 您可以为此使用bluebirdfs-extrafs-promise

但是, node 自带的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

从循环中调用异步方法是不好的。 这是因为每次循环迭代都会延迟到整个异步操作完成。 这不是很高效。 它还避免了async / await的并行化优势。

更好的解决方案是一次创建所有 Promise,然后使用Promise.all()访问结果。 否则,每个后续操作将在前一个操作完成之前不会开始。

因此,代码可以重构如下;

const printFiles = async () => {
  const files = await getFilePaths();
  const results = [];
  files.forEach((file) => {
    results.push(fs.readFile(file, 'utf8'));
  });
  const contents = await Promise.all(results);
  console.log(contents);
}

一个重要的警告是: await + for .. of方法和forEach + async方法实际上具有不同的效果。

在真正for循环中await将确保所有异步调用都被一一执行。 forEach + async方式会同时触发所有的 Promise,速度更快但有时会让人不知所措(如果你做一些数据库查询或访问一些有容量限制的 Web 服务,并且不想一次触发 100,000 个调用)。

如果您不使用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);
    }
}

上述两种解决方案都有效,但是,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)))

只是添加到原来的答案

  • 原始答案中的并行阅读语法有时令人困惑且难以阅读,也许我们可以用不同的方法编写它
async function printFiles() {
  const files = await getFilePaths();
  const fileReadPromises = [];

  const readAndLogFile = async filePath => {
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
    return contents;
  };

  files.forEach(file => {
    fileReadPromises.push(readAndLogFile(file));
  });

  await Promise.all(fileReadPromises);
}

  • 对于顺序操作,不仅仅是for...of ,正常的 for 循环也可以工作
async function printFiles() {
  const files = await getFilePaths();

  for (let i = 0; i < files.length; i++) {
    const file = files[i];
    const contents = await fs.readFile(file, "utf8");
    console.log(contents);
  }
}

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

如果一个被拒绝, Promise.all拒绝所有的承诺。

所以,使用递归。

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

附言

readFilesQueueprintFiles之外导致由console.log引入的副作用*,最好模拟、测试和/或监视,所以拥有一个返回内容的函数(旁注)并不酷。

因此,代码可以简单地设计为:三个独立的“纯”函数**并且没有引入副作用,处理整个列表,并且可以轻松修改以处理失败的情况。

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)

未来编辑/当前状态

Node 支持顶级等待(它还没有插件,不会有并且可以通过和谐标志启用),它很酷但不能解决一个问题(从战略上讲,我只在 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,它们容易出错,并将该错误与应用程序分开处理。

今天我遇到了多种解决方案。 在 forEach 循环中运行异步等待函数。 通过构建包装器,我们可以做到这一点。

关于它如何在内部工作的更详细说明,对于本机 forEach 以及为什么它无法进行异步函数调用以及各种方法的其他详细信息在此处的链接中提供

可以通过多种方式完成,如下所示,

方法1:使用包装器。

await (()=>{
     return new Promise((resolve,reject)=>{
       items.forEach(async (item,index)=>{
           try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
           count++;
           if(index === items.length-1){
             resolve('Done')
           }
         });
     });
    })();

方法二:和Array.prototype的泛型函数一样使用

Array.prototype.forEachAsync.js

if(!Array.prototype.forEachAsync) {
    Array.prototype.forEachAsync = function (fn){
      return new Promise((resolve,reject)=>{
        this.forEach(async(item,index,array)=>{
            await fn(item,index,array);
            if(index === array.length-1){
                resolve('done');
            }
        })
      });
    };
  }

用法 :

require('./Array.prototype.forEachAsync');

let count = 0;

let hello = async (items) => {

// Method 1 - Using the Array.prototype.forEach 

    await items.forEachAsync(async () => {
         try{
               await someAPICall();
           } catch(e) {
              console.log(e)
           }
        count++;
    });

    console.log("count = " + count);
}

someAPICall = () => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve("done") // or reject('error')
        }, 100);
    })
}

hello(['', '', '', '']); // hello([]) empty array is also be handled by default

方法3:

使用 Promise.all

  await Promise.all(items.map(async (item) => {
        await someAPICall();
        count++;
    }));

    console.log("count = " + count);

方法 4:传统的 for 循环或现代的 for 循环

// Method 4 - using for loop directly

// 1. Using the modern for(.. in..) loop
   for(item in items){

        await someAPICall();
        count++;
    }

//2. Using the traditional for loop 

    for(let i=0;i<items.length;i++){

        await someAPICall();
        count++;
    }


    console.log("count = " + count);

您可以使用Array.prototype.forEach ,但 async/await 不是那么兼容。 这是因为从异步回调返回的 Promise 预计会被解析,但Array.prototype.forEach不会从其回调的执行中解析任何 Promise。 因此,您可以使用 forEach,但您必须自己处理承诺解决方案。

这是一种使用Array.prototype.forEach连续读取和打印每个文件的方法

async function printFilesInSeries () {
  const files = await getFilePaths()

  let promiseChain = Promise.resolve()
  files.forEach((file) => {
    promiseChain = promiseChain.then(() => {
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    })
  })
  await promiseChain
}

这是一种并行打印文件内容的方法(仍然使用Array.prototype.forEach

async function printFilesInParallel () {
  const files = await getFilePaths()

  const promises = []
  files.forEach((file) => {
    promises.push(
      fs.readFile(file, 'utf8').then((contents) => {
        console.log(contents)
      })
    )
  })
  await Promise.all(promises)
}

目前 Array.forEach 原型属性不支持异步操作,但我们可以创建自己的 poly-fill 来满足我们的需求。

// 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}

就是这样! 您现在可以在这些操作之后定义的任何数组上使用 async forEach 方法。

让我们测试一下...

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

... 等等 :)

需要注意的一些事项:

  • 您的 iteratorFunction 必须是异步函数或承诺
  • Array.prototype.<yourAsyncFunc> = <yourAsyncFunc>之前创建的任何数组都不会提供此功能

要查看如何出错,请在方法末尾打印 console.log。

一般可能出错的事情:

  • 任意顺序。
  • printFiles 可以在打印文件之前完成运行。
  • 表现不佳。

这些并不总是错误的,但经常出现在标准用例中。

通常,使用 forEach 将导致除最后一个之外的所有结果。 它会在不等待函数的情况下调用每个函数,这意味着它告诉所有函数开始然后完成而不等待函数完成。

import fs from 'fs-promise'

async function printFiles () {
  const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'))

  for(const file of files)
    console.log(await file)
}

printFiles()

这是本机 JS 中的一个示例,它将保持顺序,防止函数过早返回并在理论上保持最佳性能。

这将:

  • 启动所有文件读取以并行发生。
  • 通过使用 map 将文件名映射到等待的承诺来保留顺序。
  • 按照数组定义的顺序等待每个承诺。

使用此解决方案,第一个文件将在可用时立即显示,而无需先等待其他文件可用。

它还将同时加载所有文件,而不必等待第一个文件完成才能开始读取第二个文件。

此版本和原始版本的唯一缺点是,如果一次启动多个读取,则由于一次可能发生更多错误,因此处理错误会更加困难。

对于一次读取文件的版本,然后将在失败时停止,而不会浪费时间尝试读取更多文件。 即使有一个精心设计的取消系统,也很难避免它在第一个文件上失败,但也已经读取了大多数其他文件。

性能并不总是可预测的。 虽然许多系统使用并行文件读取会更快,但有些系统更喜欢顺序读取。 有些是动态的,可能会在负载下发生变化,提供延迟的优化并不总是在激烈的争用下产生良好的吞吐量。

该示例中也没有错误处理。 如果某些事情要求它们要么全部成功显示,要么根本不显示,它不会那样做。

建议在每个阶段使用 console.log 和假文件读取解决方案(改为随机延迟)进行深入实验。 尽管许多解决方案在简单的情况下似乎都做同样的事情,但它们都有细微的差异,需要一些额外的审查才能消除。

使用这个模拟来帮助区分解决方案:

(async () => {
  const start = +new Date();
  const mock = () => {
    return {
      fs: {readFile: file => new Promise((resolve, reject) => {
        // Instead of this just make three files and try each timing arrangement.
        // IE, all same, [100, 200, 300], [300, 200, 100], [100, 300, 200], etc.
        const time = Math.round(100 + Math.random() * 4900);
        console.log(`Read of ${file} started at ${new Date() - start} and will take ${time}ms.`)
        setTimeout(() => {
          // Bonus material here if random reject instead.
          console.log(`Read of ${file} finished, resolving promise at ${new Date() - start}.`);
          resolve(file);
        }, time);
      })},
      console: {log: file => console.log(`Console Log of ${file} finished at ${new Date() - start}.`)},
      getFilePaths: () => ['A', 'B', 'C', 'D', 'E']
    };
  };

  const printFiles = (({fs, console, getFilePaths}) => {
    return async function() {
      const files = (await getFilePaths()).map(file => fs.readFile(file, 'utf8'));

      for(const file of files)
        console.log(await file);
    };
  })(mock());

  console.log(`Running at ${new Date() - start}`);
  await printFiles();
  console.log(`Finished running at ${new Date() - start}`);
})();

使用 Task、futurize 和一个可遍历的 List,你可以简单地做

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

这是在 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的原始问题

在 forEach 循环中使用 async/await 有什么问题吗? ...

@Bergi's selected answer在一定程度上涵盖了该答案,该答案显示了如何串行和并行处理。 然而,并行性还有其他问题 -

  1. 订单—— @chharvey指出——

例如,如果一个非常小的文件在一个非常大的文件之前完成读取,它将首先被记录,即使文件数组中小文件在大文件之后也是如此。

  1. 可能一次打开太多文件——Bergi 在另一个答案下的评论

一次打开数千个文件以同时读取它们也不好。 人们总是需要评估顺序、并行或混合方法是否更好。

因此,让我们解决这些问题,展示简洁明了的实际代码,并且使用第三方库。 可以轻松剪切、粘贴和修改的东西。

并行读取(一次全部),串行打印(每个文件尽可能早)。

最简单的改进是在@Bergi 的回答中执行完全并行,但做一个小改动,以便在保留 order 的同时尽快打印每个文件。

async function printFiles2() {
  const readProms = (await getFilePaths()).map((file) =>
    fs.readFile(file, "utf8")
  );
  await Promise.all([
    await Promise.all(readProms),                      // branch 1
    (async () => {                                     // branch 2
      for (const p of readProms) console.log(await p);
    })(),
  ]);
}

上面,两个单独的分支同时运行。

  • 分支 1:一次并行读取,
  • 分支 2:串行读取以强制排序,但无需等待

那很简单。

在并发限制的情况下并行读取,串行打印(每个文件尽可能早)。

“并发限制”意味着同时读取的文件不超过N个。
就像一家商店一次只允许这么多顾客进来(至少在 COVID 期间)。

首先介绍一个辅助函数——

function bootablePromise(kickMe: () => Promise<any>) {
  let resolve: (value: unknown) => void = () => {};
  const promise = new Promise((res) => { resolve = res; });
  const boot = () => { resolve(kickMe()); };
  return { promise, boot };
}

bootablePromise(kickMe:() => Promise<any>)将函数kickMe作为启动任务的参数(在我们的例子中为readFile )。 但它不会立即启动。

bootablePromise返回几个属性

  • promise类型的Promise
  • boot类型函数()=>void

promise人生有两个阶段

  1. 承诺开始一项任务
  2. 作为一个承诺完成它已经开始的任务。

boot()被调用时, promise从第一个状态转换到第二个状态。

bootablePromise用于printFiles --

async function printFiles4() {
  const files = await getFilePaths();
  const boots: (() => void)[] = [];
  const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
  const bootableProms = files.map((file,pidx) => {
    const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
    boots.push(boot);
    set.add(promise.then(() => ({ pidx })));
    return promise;
  });
  const concurLimit = 2;
  await Promise.all([
    (async () => {                                       // branch 1
      let idx = 0;
      boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
      while (idx<boots.length) {
        const { pidx } = await Promise.race([...set]);
        set.delete([...set][pidx]);
        boots[idx++]();
      }
    })(),
    (async () => {                                       // branch 2
      for (const p of bootableProms) console.log(await p);
    })(),
  ]);
}

和以前一样有两个分支

  • 分支 1:用于运行和处理并发性。
  • 分支 2:用于打印

现在的区别是允许并发运行的承诺永远不会超过concurLimit

重要的变量是

  • boots :要调用的函数数组以强制其相应的转换承诺。 它仅用于分支 1。
  • set :在随机访问容器中有承诺,因此一旦实现就可以轻松删除它们。 此 contianer 仅在分支 1 中使用。
  • bootableProms :这些是最初在set中的 smae 承诺,但它是一个数组而不是一个集合,并且该数组永远不会改变。 它仅用于分支 2。

使用模拟fs.readFile运行,时间如下(文件名与时间以毫秒为单位)。

const timeTable = {
  "1": 600,
  "2": 500,
  "3": 400,
  "4": 300,
  "5": 200,
  "6": 100,
};

可以看到像这样的测试运行时间,表明并发正在工作——

[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005

打字稿游乐场沙箱中作为可执行文件提供

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

我认为重要的是要解决为什么不会发生这种情况。

想想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);
});

forEachfor循环中,我们调用cb(arr[i]) ,最终成为logFile(file) logFile函数内部有一个await ,所以for循环可能会在继续i++之前等待这个await

不,不会的。 令人困惑的是,这不是await的工作方式。 文档

等待拆分执行流程,允许异步函数的调用者恢复执行。 在 await 推迟了 async 函数的继续执行之后,随后的语句就会执行。 如果此 await 是其函数执行的最后一个表达式,则继续通过向函数的调用者返回待处理的 Promise 以完成 await 的函数并恢复该调用者的执行。

因此,如果您有以下情况,则不会在"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();

回到forEachforEach就像mainlogFile就像logNumbers main不会因为logNumbers做了一些await就停止,而forEach也不会因为logFile做了一些await就停止。

与 Antonio Val 的p-iteration类似,另一种 npm 模块是async-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();

async-af

在 2022 年,我仍然建议使用外部库来处理所有这些异步流程。 我已经为类似的事情创建了很多模块🔗

你的例子是:

import fs from 'fs-promise'
import alot from 'alot'

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

    await alot(files)
        .forEachAsync(async file => {
            let content = await fs.readFile(file, 'utf8');
            console.log(content);
        })
        .toArrayAsync({ threads: 4 });
    }
}
printFiles()

对于简单的示例, async for..of肯定可以完成这项工作,但是一旦任务变得更加复杂,您就必须为此使用一些实用程序。

Alot 有许多其他可以链接的方法,例如mapAsyncfilterAsyncgroupAsync等。

举个例子:

  • 加载带有产品元数据的 JSON 文件
  • 提取ProductID
  • 从服务器加载产品
  • 过滤价格 > 100 美元的商品
  • 按价格升序排列
  • 进入前 50 名

import fs from 'fs-promise'
import alot from 'alot'
import axios from 'axios'
import { File } from 'atma-io'

let paths = await getFilePaths();
let products = await alot(paths)
    .mapAsync(async path => await File.readAsync<IProductMeta>(path))
    .mapAsync(async meta => await axios.get(`${server}/api/product/${meta.productId}`))
    .mapAsync(resp => resp.data)
    .filterAsync(product => product.price > 100)
    .sortBy(product => product.price, 'asc')
    .takeAsync(50)
    .toArrayAsync({ threads: 5, errors: 'include' });

如果您想同时迭代所有元素:

async function asyncForEach(arr, fn) {
  await Promise.all(arr.map(fn));
}

如果您想非并发地迭代所有元素(例如,当您的映射函数有副作用或一次在所有数组元素上运行映射器时资源成本太高):

选项 A:承诺

function asyncForEachStrict(arr, fn) {
  return new Promise((resolve) => {
    arr.reduce(
      (promise, cur, idx) => promise
        .then(() => fn(cur, idx, arr)),
      Promise.resolve(),
    ).then(() => resolve());
  });
}

选项 B:异步/等待

async function asyncForEachStrict(arr, fn) {
  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    await fn(cur, idx, arr);
  }
}

如果你不能使用 async/await (IE11、旧打包器等),那么你可以试试这个递归函数。 我使用fetch作为我的异步调用,但您可以使用任何返回承诺的函数。

var urlsToGet = ['https://google.com', 'https://yahoo.com'];

fetchOneAtATime(urlsToGet);

function fetchOneAtATime(urls) {
    if (urls.length === 0) {
        return;
    }
    fetch(urls[0]).finally(() => fetchOneAtATime(urls.slice(1)));
}

这是一个简单的解决方案

const A = new Promise((resolve) => {
  setTimeout(resolve, 300, 'ankit');
});

const B = new Promise((resolve) => {
  setTimeout(resolve, 50, '123');
});

const C = new Promise((resolve) => {
  setTimeout(resolve, 500, '345');
});

const D = [A, B, C];
Promise.all(D).then(console.log);


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

在 forEach 循环中使用 async/await 没有错 在循环中使用 async/await 可以使您的代码更易于阅读和理解,因为它允许您使用看起来同步的 await 表达式来暂停循环迭代,直到异步操作完成。

但是,在 forEach 循环中使用 async/await 时,您应该注意一些潜在的问题

1) forEach 循环不等待异步操作完成才继续下一次迭代。 这意味着循环可能会在所有异步操作完成之前完成,这可能会导致意外行为。

2)如果任何异步操作抛出错误,错误将不会被 forEach 循环周围的 try/catch 块捕获 相反,您需要在循环内添加一个 try/catch 块来捕获错误。

3)异步动作完成的顺序可能与它们开始的顺序不同。 这意味着输出可能与原始数组的顺序不同。

希望能帮助到你!

我会使用经过充分测试(每周数百万次下载)的pifyasync模块。 如果您不熟悉异步模块,我强烈建议您查看它的文档 我已经看到多个开发人员浪费时间重新创建其方法,或者更糟糕的是,当高阶异步方法可以简化代码时,他们制作了难以维护的异步代码。

 const async = require('async') const fs = require('fs-promise') const pify = require('pify') async function getFilePaths() { return Promise.resolve([ './package.json', './package-lock.json', ]); } async function printFiles () { const files = await getFilePaths() await pify(async.eachSeries)(files, async (file) => { // <-- run in series // await pify(async.each)(files, async (file) => { // <-- run in parallel const contents = await fs.readFile(file, 'utf8') console.log(contents) }) console.log('HAMBONE') } printFiles().then(() => { console.log('HAMBUNNY') }) // ORDER OF LOGS: // package.json contents // package-lock.json contents // HAMBONE // HAMBUNNY ```

对于 TypeScript 用户,一个合适的通用 asyncMap()

  • 使用Promise.all(array.map(iterator))具有正确的类型,因为 TypeScript 的标准库支持已经处理了泛型。
  • 但是每次需要异步映射时复制粘贴Promise.all(array.map(iterator))是不好的,而且Promise.all(array.map(iterator))不是特别愉快,所以有一个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).toEqual(["Finished 1", "Finished 2", "Finished 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));
};
exports.getTat = async function () {
  for (const tatfcp of resp[0]) {
    const getProductResponcekey = params.pincode + '-' + tatfcp.productid + '-' + result[tatfcp.productid].reqQty + '-' + tatfcp.groups[0].dispatchwarehouseid;
    const redisResp = await redis.getRedis(getProductResponcekey);
    if (redisResp) {
      products.push(redisResp[0]);
      console.log('redis', redisResp[0]);
    } else {
      const getProductResponceData = await getProductResponce(resp[1], resp[2], params.pincode, tatfcp, data[1], data[2], data[8], gstandvendordata[1], data[9]);
      products.push(getProductResponceData);
      redis.setRedis(getProductResponcekey, getProductResponceData, config.redis.expiryTime1Day);
    }
  }
};

这是我的解决方案

作为替代方案,您可以使用 utils-decorators 库,其中包含ThrottleAsync装饰器:

  import { throttleAsync } from 'utils-decorators';
  import { DataProvider, DataDto }

  class Example1 {

    @throttleAsync(2)
    getData(): Promise<DataDto> {
      // ...
    }
  }

或者throttleAsyncify生成器函数

 import { throttleAsyncify } from 'utils-decorators';
 import { dataProvider } from './data-provider';

  const throttledDataProvider = throttleAsyncify(dataProvider.getData, 2);

这是文档的链接: https : //vlio20.github.io/utils-decorators/#throttleAsync

这是图书馆的链接: https : //github.com/vlio20/utils-decorators

您可以使用npm install utils-decorators安装它

注意:如果要按顺序运行执行,只需将我分享的示例中的值从 2 更改为 1。 通过这种方式,您可以控制并行执行的调用数量。

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

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM