Есть ли проблемы с использованием async / await в цикле forEach? Я пытаюсь перебрать массив файлов и 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 в функции более высокого порядка, как это, поэтому я просто хотел спросить, есть ли какие-либо проблемы с этим.

1558
Saad 1 Июн 2016 в 21:55

17 ответов

Лучший ответ

Конечно, код работает, но я уверен, что он не работает так, как вы ожидаете. Он просто запускает несколько асинхронных вызовов, но функция 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 возвращает обещание, но вы отбрасываете их, а не ожидаете. Просто используйте 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)
  }));
}
1961
mesqueeb 8 Фев 2020 в 23:30

В настоящее время свойство прототипа 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}

Вот и все! Теперь у вас есть асинхронный метод 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>, не будут иметь эту функцию доступной
2
mikemaccana 26 Апр 2019 в 09:22

Оба вышеупомянутых решения работают, однако, Антонио выполняет работу с меньшим количеством кода, вот как это помогло мне разрешить данные из моей базы данных, от нескольких разных дочерних ссылок, а затем поместить их все в массив и разрешить их в обещании, в конце концов, сделано:

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)))
4
Hooman Askari 26 Авг 2017 в 10:47

В дополнение к @ ответу Берги я хотел бы предложить третий вариант. Это очень похоже на 2-й пример @ 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(), не обязательно должна быть async, так как fs.readFile в любом случае возвращает объект Promise. Следовательно, promises - это массив объектов Promise, которые можно отправить в Promise.all().

В ответе @ Bergi консоль может записывать содержимое файла в порядке их чтения. Например, если действительно маленький файл заканчивает чтение перед действительно большим файлом, он сначала регистрируется, даже если маленький файл появляется после большого файла в массиве files. Тем не менее, в моем методе выше, вы гарантированно, консоль будет записывать файлы в том же порядке, что и предоставленный массив.

5
chharvey 11 Окт 2019 в 00:57

Решение Берги прекрасно работает, когда fs основан на обещаниях. Вы можете использовать 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') обязательно принимает функцию в качестве 3-го аргумента, в противном случае выдает ошибку:

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
4
myDoggyWritesCode 26 Май 2019 в 22:08

Сегодня я столкнулся с несколькими решениями для этого. Запуск асинхронных функций ожидания в цикле 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')
           }
         });
     });
    })();

Способ 2: Использование так же, как универсальная функция 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: Традиционный для цикла или современный для цикла

// 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);
1
PranavKAndro 27 Ноя 2019 в 05:42

Вместо Promise.all в сочетании с Array.prototype.map (что не гарантирует порядок разрешения Promise), я использую Array.prototype.reduce, начиная с разрешенного {{X4 } } :

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());
}
57
Timothy Zorn 3 Мар 2019 в 03:38

Аналогично 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) что записывает результаты обещаний:

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

0
Scott Rudiger 21 Июн 2018 в 16:55

Довольно просто вставить пару методов в файл, который будет обрабатывать асинхронные данные в последовательном порядке и придавать вашему коду более привычный вид. Например:

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));
}
3
Jay Edwards 26 Сен 2017 в 09:07

Одним из важных предостережений является то, что метод await + for .. of и способ forEach + async действительно имеют разный эффект.

Наличие await внутри реального цикла for обеспечит выполнение всех асинхронных вызовов один за другим. И способ forEach + async будет запускать все обещания одновременно, что быстрее, но иногда перегружено (, если вы делаете запрос к БД или посещаете некоторые веб-службы с ограничениями по объему и не хотите сделать 100 000 звонков одновременно).

Вы также можете использовать reduce + promise (менее элегантно), если вы не используете async/await и хотите убедиться, что файлы читаются один за другим .

files.reduce((lastPromise, file) => 
 lastPromise.then(() => 
   fs.readFile(file, 'utf8')
 ), Promise.resolve()
)

Или вы можете создать forEachAsync, чтобы помочь, но в основном используйте его для цикла.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
2
LeOn - Han Li 24 Сен 2017 в 20:00

Вот несколько 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));
}

Примечание , хотя вы можете включать это в свой собственный код, вы не должны включать это в библиотеки, которые вы распространяете среди других (чтобы не загрязнять их глобальные объекты).

22
mikemaccana 26 Апр 2019 в 09:47

С ES2018 вы можете значительно упростить все вышеперечисленные ответы на:

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

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

См. Спецификацию: предложение-асинхронная итерация


2018-09-10: В последнее время этому ответу уделяется много внимания, пожалуйста, см. Сообщение в блоге Акселя Раушмайера для получения дополнительной информации об асинхронной итерации: ES2018: асинхронная итерация

175
Rich Warrior 21 Авг 2019 в 18:23

Модуль p-iteration в npm реализует методы итерации Array, поэтому их можно использовать в очень прямой путь с асинхронным / жду.

Пример с вашим случаем:

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);
  });
})();
30
Antonio Val 17 Окт 2019 в 05:59

Вроде ответ @Bergi, но с одним дифференциалом.

Promise.all отклоняет все обещания, если один отклонен.

Итак, используйте рекурсию.

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

    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
    }

    await readFilesQueue(files)
}

printFiles()

PS

readFilesQueue находится внутри printFiles, вызывая побочный эффект, представленный 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
}

Или измените имена переменных, как угодно ...

0
lukaswilkeer 21 Дек 2019 в 01:11

Я бы использовал хорошо проверенные (миллионы загрузок в неделю) pify и async. Если вы не знакомы с модулем асинхронности, я настоятельно рекомендую вам ознакомиться с его документами . Я видел, как несколько разработчиков тратили время на воссоздание своих методов или, что еще хуже, создание сложного в обслуживании асинхронного кода, когда асинхронные методы более высокого порядка упростят код.

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
```
-3
Zachary Ryan Smith 4 Фев 2018 в 16:03

Используя 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 */
}

Если вам действительно нужна большая гибкость в кодировании, вы можете просто сделать это (для удовольствия я использую предложенный Оператор пересылки труб)

import { curry, flip } from 'ramda'

export const readFile = fs.readFile 
  |> future,
  |> curry,
  |> flip

export const readFileUtf8 = readFile('utf-8')

PS - я не пробовал этот код на консоли, возможно, есть некоторые опечатки ... "прямой фристайл, с верхней части купола!" как сказали бы дети 90-х. :-п

2
Babakness 3 Апр 2018 в 22:51

Чтобы увидеть, как это может пойти не так, напечатайте 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, который будет сохранять порядок, предотвращать преждевременный возврат функции и теоретически сохранять оптимальную производительность.

Это будет:

  • Инициируйте все операции чтения файлов параллельно.
  • Сохраните порядок с помощью карты, чтобы сопоставить имена файлов с обещаниями ждать.
  • Ждите каждого обещания в порядке, определенном массивом.

С этим решением первый файл будет показан, как только он станет доступен, без необходимости ждать, пока другие будут доступны в первую очередь.

Он также будет загружать все файлы одновременно, вместо того, чтобы ждать окончания первого, прежде чем можно будет начать чтение второго файла.

Единственный недостаток этого и оригинальной версии заключается в том, что если запускать несколько операций чтения одновременно, то более трудно обрабатывать ошибки из-за наличия большего количества ошибок, которые могут происходить одновременно.

С версиями, которые читают файл за раз, затем останавливается на сбое, не тратя время на попытки прочитать больше файлов. Даже при сложной системе отмены может быть трудно избежать сбоя первого файла, но уже можно прочитать и большинство других файлов.

Производительность не всегда предсказуема. Хотя многие системы будут работать быстрее с параллельным чтением файлов, некоторые предпочтут последовательное чтение. Некоторые из них являются динамическими и могут смещаться под нагрузкой. Оптимизация, обеспечивающая задержку, не всегда дает хорошую пропускную способность в условиях сильной конкуренции.

В этом примере также нет обработки ошибок. Если что-то требует, чтобы они либо все были успешно показаны, либо нет вообще, это не будет сделано.

На каждом этапе рекомендуется проводить подробные эксперименты с 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}`);
})();

0
jgmjgm 14 Окт 2019 в 19:16