it-roy-ru.com

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

566
saadq

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

С 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

83
Francisco Mateo

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

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

printFiles()

p-итерация

22
Antonio Val

Вместо 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());
}
20
Timothy Zorn

Вот некоторые асинхронные прототипы 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));
}
11
Matt

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

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)))
2
Hooman Askari

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

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));
}
2
Jay Edwards

В дополнение к @ Bergi's answer , я хотел бы предложить третий вариант. Это очень похоже на второй пример @ 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. Тем не менее, в моем методе выше, вы гарантированно, консоль будет записывать файлы в том же порядке, в котором они были прочитаны.

1
chharvey

В настоящее время свойство прототипа 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>, не будут иметь эту функцию доступной
1
Beau

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

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

import { curry, flip } from 'ramda'

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

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

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

1
Babakness

Решение Берги прекрасно работает, когда 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
0
myDoggyWritesCode

Аналогично Антонио Валу 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

Один важный caveat : метод 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);
    }
}
0
Leon li