Использование async/wait с циклом forEach

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

Ответ 1

Конечно, код работает, но я уверен, что он не работает так, как вы ожидаете. Он просто запускает несколько асинхронных вызовов, но функция 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)
  }));
}

Ответ 2

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

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

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

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


2018-09-10: Этот ответ в последнее время привлекает к себе большое внимание. Дополнительную информацию об асинхронной итерации смотрите в блоге Акселя Раушмайера: ES2018: асинхронная итерация

Ответ 3

Вместо Promise.all в сочетании с Array.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());
}

Ответ 4

Модуль p-iteration в npm реализует методы итерации 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);
  });
})();

Ответ 5

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

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

Ответ 6

В дополнение к ответу @Bergis, я хотел бы предложить третий вариант. Это очень похоже на пример @Bergis 2nd, но вместо того, чтобы ждать каждого 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().

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 9

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

Имея await внутри реального цикла for, убедитесь, что все асинхронные вызовы выполняются один за другим. И forEach + async способ будет отключать все promises в то же время, что быстрее, но иногда перегружается (, если вы выполняете какой-либо запрос БД или посещаете некоторые веб-службы с ограничениями объема и не хотите для запуска 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);
    }
}

Ответ 10

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

Ответ 11

В настоящее время свойство прототипа Array.forEach не поддерживает асинхронные операции, но мы можем создать наше собственное poly-fill для удовлетворения наших потребностей.

// Example of asyncForEach Array poly-fill for NodeJs
// file: asyncForEach.js
// Define asynForEach function 
async function asyncForEach(callback){
  let indexer = 0
  for(let data of this){
    await callback(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 your name'
  ,'What your favorite programming language'
  ,'What 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(callback){
  let newMap = []
  let indexer = 0
  for(let data of this){
    newMap[indexer] = await callback(data, indexer, this)
    indexer++
  }
  return newMap
}

Array.prototype.asyncMap = asyncMap

... и так далее :)

Некоторые вещи на заметку:

  • Ваш обратный вызов должен быть асинхронной функцией или обещанием
  • Любые массивы, созданные до Array.prototype.<yourAsyncFunc> = <yourAsyncFunc> не будут иметь эту функцию доступной

Ответ 12

Решение Bergi хорошо работает, когда fs основан на обещаниях. Для этого вы можете использовать bluebird, fs-extra или fs-promise.

Тем не менее, решение для нативного fs libary выглядит следующим образом:

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

Ответ 13

Подобно 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 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

Ответ 14

Чтобы увидеть, как это может пойти не так, напечатайте 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}');
})();

Ответ 15

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

  const async = require ('async')
const fs = require ('fs-prom')
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) (файлы, async (файл) = > {//< - запускать последовательно
 //await  pify (async.each) (файлы, async (файл) = > {//< - выполняется параллельно
   const content = await fs.readFile(файл, 'utf8')
   console.log(содержание)
 })
 console.log( 'HAMBONE')
}

printFiles(). then (() = > {
   console.log( 'HAMBUNNY')
})
// ПОРЯДОК ЛОГ:
// содержимое package.json
// содержимое пакета-lock.json
//HAMBONE 
//HAMBUNNY 
`` `Код>