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


1133

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

Ответы:


2159

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

33
Не могли бы вы объяснить, почему это for ... of ...работает?
Demonbane

84
хорошо, я знаю почему ... Использование Babel преобразует async/ awaitв функцию генератора, а использование forEachозначает, что каждая итерация имеет отдельную функцию генератора, которая не имеет ничего общего с другими. поэтому они будут выполняться независимо друг от друга и не имеют контекста next()с другими. На самом деле, простой for()цикл также работает, потому что итерации также находятся в одной функции генератора.
Demonbane

21
@Demonbane: Короче говоря, потому что он был разработан для работы :-) awaitприостанавливает текущую оценку функции , включая все управляющие структуры. Да, это очень похоже на генераторы в этом отношении (именно поэтому они используются для полизаполнения async / await).
Берги

3
@ arve0 Не совсем, asyncфункция сильно отличается от Promiseобратного вызова исполнителя, но да, mapобратный вызов возвращает обещание в обоих случаях.
Берги

5
Когда вы придете, чтобы узнать об обещаниях JS, но вместо этого используйте полчаса перевод латинского. Надеюсь, вы гордитесь @ Берги;)
Феликс Ганьон-Гренье

190

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

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

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

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


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


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

8
Не должно ли это быть содержимым вместо файла в итераторе
FluffyBeing

10
Почему люди голосуют против этого ответа? Внимательно посмотрите на ответ, вопрос и предложение. После ofдолжна быть асинхронная функция, которая будет возвращать массив. Это не работает, и Франциско сказал;
Евгений Герасимчук

3
Полностью согласен с @AntonioVal. Это не ответ.
Евгений Герасимчук

2
Хотя я согласен, что это не ответ, голосование по предложению - это способ повысить его популярность, потенциально делая его доступным для последующего использования.
Роберт Молина

62

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

1
Это работает отлично, большое спасибо. Не могли бы вы объяснить, что здесь происходит с Promise.resolve()и await promise;?
parrker9

1
Это довольно круто. Правильно ли я считаю, что файлы будут прочитаны по порядку, а не сразу?
GollyJer

1
@ parrker9 Promise.resolve()возвращает уже разрешенный Promiseобъект, поэтому reduceон Promiseдолжен начинаться с. await promise;будет ждать решения последней Promiseв цепочке. @GollyJer Файлы будут обрабатываться последовательно, по одному за раз.
Тимоти Зорн

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

1
@ Скажи, ты имеешь в виду последовательный, а не синхронный. Это все еще асинхронно - если запланированы другие вещи, они будут выполняться здесь между итерациями.
Тимоти Цорн

33

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

1
Мне это нравится, так как он имеет те же функции / методы, что и сам JS - в моем случае мне нужно было, someа не forEach. Спасибо!
mikemaccana

26

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

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


1
Хотя я не решался бы добавлять вещи непосредственно в прототип, это хорошая асинхронная реализация forEach
DaniOcean

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

1
@estus Чтобы избежать загрязнения чужого кода. Если код принадлежит нашей личной организации, а глобалы находятся в хорошо идентифицированном файле ( globals.jsбыло бы хорошо), мы можем добавлять глобалы по своему усмотрению.
mikemaccana

1
@mikemaccana Это чтобы избежать общепринятых плохих практик. Это правда, это может быть сделано, если вы используете только сторонний код, что случается редко. Проблема в том, что когда вы используете сторонние библиотеки, может быть какой-то другой парень, который чувствует то же самое и модифицирует те же глобальные переменные, просто потому, что это было хорошей идеей в то время, когда была написана библиотека.
Estus Колба

1
@estus Конечно. Я добавил предупреждение к вопросу, чтобы сохранить (не особенно продуктивное) обсуждение здесь.
mikemaccana

7

В дополнение к ответу @ Bergi , я хотел бы предложить третий вариант. Это очень похоже на второй пример @ 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
Я уверен, что вы не правы: я уверен, что ваш метод также может читать файлы не по порядку. Да, он будет записывать вывод в правильном порядке (из-за await Promise.all), но файлы могли быть прочитаны в другом порядке, что противоречит вашему утверждению: «вы гарантированно, что консоль будет записывать файлы в том же порядке, в каком они есть». читать".
Venryx

1
@Venryx Вы правы, спасибо за исправление. Я обновил свой ответ.
Чарви

6

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

5

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

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

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

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
Незначительное дополнение, не забудьте обернуть ваши await / asyncs в блоки try / catch !!
Джей Эдвардс

4

Как ответ @ Берги, но с одним отличием.

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

PS

readFilesQueueЭто printFilesпобочный эффект *, введенный console.log, лучше издеваться, тестировать и / или шпионить, поэтому не круто иметь функцию, которая возвращает контент (sidenote).

Следовательно, код можно просто спроектировать следующим образом: три отдельные функции, которые являются «чистыми» ** и не вызывают побочных эффектов, обрабатывают весь список и могут быть легко изменены для обработки неудачных случаев.

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.

** «чистый» - это апостроф, поскольку функции не чистые, и код можно конвертировать в чистую версию, когда нет вывода на консоль, только манипуляции с данными.

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


3

Одно важное предостережение : 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);
    }
}

Посмотрите, как определить метод в javascript для Array.prototype и Object.prototype, чтобы он не появлялся в цикле for . Также вам, вероятно, следует использовать ту же итерацию, что и в нативной forEach- доступ к индексам вместо того, чтобы полагаться на итеративность, - и передать индекс в обратный вызов.
Берги

Вы можете использовать Array.prototype.reduceспособ, который использует асинхронную функцию. Я показал пример в своем ответе: stackoverflow.com/a/49499491/2537258
Тимоти Цорн

3

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


3

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

3

Просто добавив к исходному ответу

  • Синтаксис параллельного чтения в исходном ответе иногда сбивает с толку и труден для чтения, может быть, мы можем написать его по-другому
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);
}
  • Для последовательной работы, а не только для ... , нормальный цикл также будет работать
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);
  }
}

2

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

2

Сегодня я столкнулся с несколькими решениями для этого. Запуск асинхронных функций ожидания в цикле 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 и 2 - просто неправильные реализации, где их Promise.allследовало бы использовать - они не учитывают ни один из многих крайних случаев.
Берги

@ Берги: Спасибо за действительные комментарии. Не могли бы вы объяснить, почему методы 1 и 2 неверны. Это также служит цели. Это работает очень хорошо. Это означает, что все эти методы возможны, в зависимости от ситуации, когда можно выбрать один из них. У меня есть бегущий пример для того же.
ПранавКандро

Сбой на пустых массивах, отсутствие обработки ошибок и, возможно, больше проблем. Не изобретай велосипед. Просто используйте Promise.all.
Берги

В определенных условиях, когда это невозможно, это будет полезно. Также обработка ошибок выполняется с помощью API forEach по умолчанию, поэтому никаких проблем. Его позаботились!
ПранавКандро

Нет, нет условий, где Promise.allэто невозможно, но async/ awaitесть. И нет, forEachабсолютно не обрабатывает никаких обещаний ошибок.
Берги

2

Это решение также оптимизировано для памяти, поэтому вы можете запустить его для 10 000 элементов данных и запросов. Некоторые из других решений приведут к сбою сервера на больших наборах данных.

В TypeScript:

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

Как пользоваться?

await asyncForEach(receipts, async (eachItem) => {
    await ...
})

2

Вы можете использовать Array.prototype.forEach, но async / await не так совместимо. Это связано с тем, что обещание, возвращаемое из асинхронного обратного вызова, ожидает разрешения, но Array.prototype.forEachне разрешает никаких обещаний при выполнении своего обратного вызова. Итак, вы можете использовать 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)
}

Первый senario идеально подходит для циклов, которые нужно
запускать

0

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


-3

Я бы использовал хорошо протестированные (миллионы загрузок в неделю) модули pify и async . Если вы не знакомы с модулем 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
```


Это шаг в неверном направлении. Вот руководство по созданию карт, которое я создал, чтобы помочь людям погрузиться в адский вызов в современную эпоху JS: github.com/jmjpro/async-package-to-async-await/blob/master/… .
Jbustamovej

Как вы можете видеть здесь , я заинтересован и открыт для использования async / await вместо async lib. Прямо сейчас я думаю, что у каждого есть время и место. Я не уверен, что async lib == "callback hell" и async / await == "современная эра JS". imo, когда async lib> async / await: 1. сложный поток (например, очередь, груз, даже авто, когда все усложняется) 2. параллелизм 3. поддержка массивов / объектов / итераций 4. обработка ошибок
Захари Райан Смит
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.