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

12 ответов


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

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

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

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

см. спецификацию:https://github.com/tc39/proposal-async-iteration


2018-09-10: этот ответ в последнее время получает много внимания, пожалуйста, см. сообщение в блоге Акселя Раушмайера для получения дополнительной информации об асинхронной итерации:http://2ality.com/2016/10/asynchronous-iteration.html


используя Promise.all() С map() немного сложно понять и многословно, но если вы хотите сделать это в простой JS, это ваш лучший выстрел, я думаю.

Если вы не против добавления модуля, я реализовал методы итерации массива, чтобы их можно было использовать очень просто с помощью 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);
  });
}

printFiles()

п-итерации


вот некоторые асинхронные прототипы forEach:

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

вместо Promise.all в сочетании с Array.prototype.map (который не гарантирует порядок, в котором Promises разрешены), я использую 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());
}

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

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

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

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

через задач, 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-х. :- p


кроме @Берги это!--10-->, Я хотел бы предложить третий вариант. Это очень похоже на 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.all().

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


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

С await внутри for loop гарантирует, что все асинхронные вызовы выполняются один за другим. И 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);
    }
}

похож на 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


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

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