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

avatar
Saad
1 июня 2016 в 18:55
1222850
34
2218

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

Источник
KernelMode
9 августа 2020 в 17:34
2

почему вы вызываете printFiles функцию более высокого порядка, если она не получает функцию в качестве аргумента или не возвращает функцию в качестве вывода?

Bergi
9 августа 2020 в 18:58
13

@KernelMode Метод forEach ​​здесь является функцией высшего порядка.

Ответы (34)

avatar
Bergi
1 июня 2016 в 19:02
4102

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

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

Demonbane
15 августа 2016 в 19:21
178

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

Bergi
15 августа 2016 в 23:28
40

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

arve0
29 марта 2017 в 11:13
1

Итак, files.map(async (file) => ... эквивалентно files.map((file) => new Promise((rej, res) => { ...?

Bergi
29 марта 2017 в 16:25
5

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

Adi Sivasankaran
23 февраля 2018 в 05:20
3

Этот ответ является лучшим: await Promise.all(_.map(arr, async (val) => {...}); решил мою проблему. Конечно, каждый асинхронный обратный вызов возвращает обещание, которого я не ждал.

Doug
2 марта 2018 в 12:47
3

Для тех, кто не знает, Promise.all возвращает один промис, который разрешается, когда разрешаются все промисы в массиве. (в основном ждет завершения всех обещаний)

doubleOrt
20 марта 2018 в 13:17
0

Я думаю, что второй абзац должен быть «Если вы хотите читать файлы параллельно, вы не можете использовать for..of»? Потому что вы можете читать файлы параллельно с forEach, просто это будет выглядеть очень некрасиво, потому что вы не можете await это.

Bergi
20 марта 2018 в 13:24
5

@Taurus Если вы не собираетесь их ждать, то for…of будет работать так же, как и forEach. Нет, я действительно имел в виду этот абзац, чтобы подчеркнуть, что в современном коде JS нет места для .forEach.

doubleOrt
20 марта 2018 в 13:26
0

@Bergi Разве это не плохо для производительности? Если вам нужно сделать 3 запроса, вы не можете сделать их все параллельно, поэтому, если каждый из них занимает одну секунду, ваша программа будет выполняться за 3 секунды. Но если вы используете map или forEach, запросы будут выполняться параллельно.

Bergi
20 марта 2018 в 13:28
0

@ Телец, я не понимаю. Что плохо для производительности по сравнению с чем?

doubleOrt
20 марта 2018 в 13:30
2

for..of плохо влияет на производительность по сравнению с forEach/map, потому что for..of будет останавливаться на каждой итерации, поэтому, если я сделаю 3 запроса, каждый запрос должен будет ждать завершения предыдущих запросов. Но с forEach/map все запросы будут выполняться параллельно.

Bergi
20 марта 2018 в 13:33
0

@Taurus No. queries.forEach(q => asyncRequest(q)); делает то же самое, что и for (const q of queries) asyncRequest(q);. Нет никакой разницы в производительности, и оба будут выполнять запросы параллельно. Конечно, ни в том, ни в другом можно ничего не ждать — как это сделать, см. мой ответ.

doubleOrt
20 марта 2018 в 13:45
0

@Bergi Я говорю об этом: gist.github.com/doubleOrt/598659adfefdd941f2a98512f0f7f078

Bergi
20 марта 2018 в 13:53
0

@Taurus Да, в основном это два подхода из моего ответа. Ни один из них не использует forEach. В чем проблема?

doubleOrt
20 марта 2018 в 13:55
0

@Bergi Я мог бы использовать forEach (будет работать так же, как map в отношении времени выполнения). Но, как вы можете видеть из моих предыдущих комментариев, я говорил либо о forEach, либо о map против for..of. Я сказал: for..of плохо влияет на производительность по сравнению с forEach/map.

doubleOrt
20 марта 2018 в 13:57
1

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

Bergi
20 марта 2018 в 14:05
0

@Taurus Но это неправильно: for…of неплох для производительности по сравнению с forEach, если используется таким же неподходящим образом, ничего не ожидая. И да, «нельзя использовать» означает «непригоден».

doubleOrt
20 марта 2018 в 14:07
0

@Bergi Вы предлагаете случай, когда вы не выполняете await любой запрос, который вы делаете внутри for..of? Если да, то хорошо, но вы согласны с тем, что ваш второй пример более эффективен, чем первый?

Bergi
20 марта 2018 в 14:14
1

@Taurus Да, это то, что я написал в своем комментарии выше. Конечно, это не имеет никакого смысла и не подходит для решения проблемы OP, независимо от того, используется ли forEach или for…of. И нет, я бы вообще не стал сравнивать эти два (for…of+await и Promise.all+map) с точки зрения «производительности» - они просто разные во многих других более важных аспектах. Выбор последовательного или параллельного выполнения — это решение, которое включает в себя множество других факторов, и, конечно же, параллельное выполнение обычно завершается быстрее.

doubleOrt
20 марта 2018 в 14:18
0

@Bergi Конечно, бывают случаи, когда for..of может быть единственным выходом (например, если каждый запрос зависит от предыдущего запроса), но новичок всегда может выбрать решение for..of, потому что оно проще.

doubleOrt
20 марта 2018 в 14:18
0

@Bergi Однако я до сих пор не понимаю, насколько это правильно: Если вы хотите читать файлы параллельно, вы действительно не можете использовать forEach.

Bergi
20 марта 2018 в 14:23
0

@Taurus Вы не можете использовать его, потому что не можете дождаться результата. Конечно, если бы вы использовали его - как это сделал OP - тогда они работали бы параллельно, но forEach абсолютно не подходит , как вы сами сказали.

doubleOrt
20 марта 2018 в 14:29
0

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

Bergi
20 марта 2018 в 14:33
0

@Taurus Я думал, что подразумевалось «и ждать, пока это закончится». Но все же, если "нельзя" означает то же, что и "абсолютно не должен", я в порядке. Упростите для новичков: им не следует никогда использовать forEach, это все, что им нужно знать.

Patrick Roberts
8 мая 2019 в 16:50
0

@Bergi, возможно, поскольку это каноническая цель обмана, вы могли бы добавить альтернативу, которая избегает синтаксиса async / await и просто возвращает обещание на map() напрямую? Я понимаю, что время console.log() больше не будет таким же, но, кажется, есть путаница по поводу того факта, что async каким-то образом не требуется для использования Promise.all() и map() .

Patrick Roberts
8 мая 2019 в 16:53
0

или вы могли бы быть так любезны, чтобы предложить альтернативные цели обмана, которые не делают уродливых вещей, таких как var promises = []; array.forEach(value => { promises.push(someAsync(value)); }); return Promise.all(promises);, потому что я вижу много таких, или похожие вопросы с лучшими ответами, но код, который слишком сложен для повторного использования, как дублирующаяся цель.

Bergi
8 мая 2019 в 17:42
0

@PatrickRoberts Я веду список даже тех, кто однажды напишет новый канонический вопрос с подробными ответами...

Maciej Krawczyk
14 декабря 2020 в 16:05
0

Для параллельного метода с картой массива, если нет необходимости что-либо делать в обратном вызове после разрешения промиса: await Promise.all(files.map((file) => fs.readFile(file, 'utf8'))) ;

masterG
14 января 2021 в 17:57
0

почему вы использовали await до Promise.all(files.map(async (file) => {, если вы ничего не делаете с возвращаемым значением? какая-то особая причина, по которой вы должны использовать ожидание, даже если вы ничего не делаете с возвращаемым значением?

Bergi
14 января 2021 в 20:17
0

@masterG Так что он ждет, прежде чем выполнить (или отклонить) обещание, которое было возвращено вызывающей стороне printFiles. Конечно, можно было бы также использовать return для Promise.all, но await — это использование по умолчанию, которое мне нужно было продемонстрировать в ответе.

010011100101
14 марта 2021 в 08:02
0

Ты спасатель жизни. На всю жизнь я не мог понять, почему Object.entries({}).forEach не возвращал желаемый результат. Переключил его на: for (let [k, v] of Object.entries({})) {...} и теперь он отлично работает. Спасибо!

leonheess
27 августа 2021 в 18:47
0

Void function return value is used

Bergi
27 августа 2021 в 21:57
0

@leonheess В чем проблема? Где (в какой позиции кода) вы получаете это предупреждение? Какой инструмент вы используете?

leonheess
28 августа 2021 в 22:07
0

@Bergi мой плохой, я использовал forEach вместо mal

Griffi
15 сентября 2021 в 21:58
0

лучший ответ ;) Полезно просто подумать, что транспилятор делает с async/await и как будет выглядеть окончательный код

Bergi
15 сентября 2021 в 22:26
0

@Griffi Почему вы все еще используете транспилятор для async/await? И во что он транспилируется?

Augustin Riedinger
13 декабря 2021 в 15:22
0

Должен быть оператор forEachAwait, который делает именно это!

Bergi
13 декабря 2021 в 15:53
0

@AugustinRiedinger Это называется for … of!

Augustin Riedinger
14 декабря 2021 в 08:45
0

Нет, for ... of ​​ведет себя не так: в одном случае он принимает функцию (которую можно returnизменить, каррировать, составить и т. д.), а в другом это просто цикл for, который должен быть continued ...

Bergi
14 декабря 2021 в 08:59
0

@AugustinRiedinger В любом случае вы не можете вернуть ничего полезного из обратного вызова forEach

Augustin Riedinger
15 декабря 2021 в 12:22
0

Что ж, они ведут себя по-разному, поэтому может иметь смысл существование обеих версий. Так же, как for ... of и forEach также сосуществуют.

Adrian Bartholomew
11 января 2022 в 16:57
0

Вам нужно будет вернуть contents.

Bergi
11 января 2022 в 17:10
0

@AdrianBartholomew OP хочет только регистрировать содержимое каждого файла и ничего не возвращать из функции. Конечно, если вы хотите создать массив содержимого файла, вы можете использовать return их из обратного вызова map.

avatar
tenbits
6 апреля 2022 в 13:10
0

В 2022 году я бы по-прежнему советовал использовать внешние библиотеки для обработки всего этого асинхронного потока. Я создал модуль много???? для подобных вещей.

Ваш пример:

import fs from 'fs-promise'
import alot from 'alot'

async function printFiles () {
    const files = await getFilePaths() // Assume this works fine

    await alot(files)
        .forEachAsync(async file => {
            let content = await fs.readFile(file, 'utf8');
            console.log(content);
        })
        .toArrayAsync({ threads: 4 });
    }
}
printFiles()

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

Alot имеет десятки других методов, которые можно объединить в цепочку, например mapAsync, filterAsync, groupAsync и т. д.

Например:

  • Загрузить файлы JSON с метаданными продуктов
  • Извлечение ProductID
  • Загрузить продукты с сервера
  • Отфильтровать те, у кого цена > 100$
  • По возрастанию цены
  • Занять 50 лучших

import fs from 'fs-promise'
import alot from 'alot'
import axios from 'axios'
import { File } from 'atma-io'

let paths = await getFilePaths();
let products = await alot(paths)
    .mapAsync(async path => await File.readAsync<IProductMeta>(path))
    .mapAsync(async meta => await axios.get(`${server}/api/product/${meta.productId}`))
    .mapAsync(resp => resp.data)
    .filterAsync(product => product.price > 100)
    .sortBy(product => product.price, 'asc')
    .takeAsync(50)
    .toArrayAsync({ threads: 5, errors: 'include' });
Bergi
6 апреля 2022 в 13:52
0

Что такое threads: 4? JS не имеет потоков

tenbits
6 апреля 2022 в 16:58
0

@Bergi Но основной слой имеет. Вся эта история с async\await означает, что цикл обработки событий ждет, пока не вернется результат. Определив threads, мы устанавливаем, сколько задач мы запускаем параллельно, остальные будут ждать, пока хотя бы одна задача (fs, network, worker и т. д.) не будет готова.

avatar
Yilmaz
1 февраля 2022 в 19:28
2
files.forEach(async (file) => {
    const contents = await fs.readFile(file, 'utf8')
})

Проблема в том, что обещание, возвращаемое функцией итерации, игнорируется forEach(). В результате все функции fs.readFile вызываются в одном и том же раунде цикла событий, что означает, что они запускаются параллельно, а не последовательно, и выполнение продолжается сразу после вызова forEach(), без ожидание завершения всех операций fs.readFile. Поскольку forEach не ждет разрешения каждого промиса, цикл фактически завершает итерацию до того, как промисы будут разрешены. В конечном итоге вы можете попытаться получить доступ к еще недоступным значениям.

avatar
Craig Hicks
17 января 2022 в 20:42
0

Оригинальный вопрос ОП

Есть ли проблемы с использованием async/await в цикле forEach? ...

был частично освещен в выбранном ответе @Bergi, который показал, как обрабатывать последовательно и параллельно. Однако есть и другие проблемы с параллелизмом -

.
  1. Заказ -- @chharvey отмечает, что -

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

  1. Возможно, открывается слишком много файлов одновременно — комментарий Берги под другим ответом

Также нехорошо открывать тысячи файлов одновременно для их одновременного чтения. Всегда нужно оценивать, какой подход лучше: последовательный, параллельный или смешанный.

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

Чтение параллельно (все сразу), печать последовательно (как можно раньше для каждого файла).

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

async function printFiles2() {
  const readProms = (await getFilePaths()).map((file) =>
    fs.readFile(file, "utf8")
  );
  await Promise.all([
    await Promise.all(readProms),                      // branch 1
    (async () => {                                     // branch 2
      for (const p of readProms) console.log(await p);
    })(),
  ]);
}

Выше две отдельные ветви выполняются одновременно.

  • ветка 1: параллельное чтение, все сразу,
  • ветвь 2: Чтение серийного номера для принудительного заказа, но ожидание не дольше, чем необходимо

Это было легко.

Параллельное чтение с ограничением одновременности, последовательная печать (как можно раньше для каждого файла).

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

Сначала вводится вспомогательная функция -

function bootablePromise(kickMe: () => Promise<any>) {
  let resolve: (value: unknown) => void = () => {};
  const promise = new Promise((res) => { resolve = res; });
  const boot = () => { resolve(kickMe()); };
  return { promise, boot };
}

Функция bootablePromise(kickMe:() => Promise<any>) принимает функция kickMe в качестве аргумента для запуска задачи (в нашем случае readFile). Но запускается не сразу.

bootablePromise возвращает пару свойств

  • promise типа Promise
  • boot типа функция ()=>void

promise имеет два этапа в жизни

  1. Обещание начать выполнение задачи
  2. Будучи обещанием завершить задачу, которую оно уже начало.

promise переходит из первого состояния во второе при вызове boot().

bootablePromise используется в printFiles --

async function printFiles4() {
  const files = await getFilePaths();
  const boots: (() => void)[] = [];
  const set: Set<Promise<{ pidx: number }>> = new Set<Promise<any>>();
  const bootableProms = files.map((file,pidx) => {
    const { promise, boot } = bootablePromise(() => fs.readFile(file, "utf8"));
    boots.push(boot);
    set.add(promise.then(() => ({ pidx })));
    return promise;
  });
  const concurLimit = 2;
  await Promise.all([
    (async () => {                                       // branch 1
      let idx = 0;
      boots.slice(0, concurLimit).forEach((b) => { b(); idx++; });
      while (idx<boots.length) {
        const { pidx } = await Promise.race([...set]);
        set.delete([...set][pidx]);
        boots[idx++]();
      }
    })(),
    (async () => {                                       // branch 2
      for (const p of bootableProms) console.log(await p);
    })(),
  ]);
}

Как и раньше есть две ветки

  • ветвь 1: для запуска и обработки параллелизма.
  • ветвь 2: Для печати

Теперь разница заключается в том, что не более чем concurLimit промисов разрешено выполнять одновременно.

Важные переменные:

  • boots: массив функций, которые нужно вызвать, чтобы принудительно выполнить переход соответствующего промиса. Используется только в ветке 1.
  • set: обещания находятся в контейнере с произвольным доступом, поэтому их можно легко удалить после выполнения. Этот контейнер используется только в ветке 1.
  • bootableProms: это обещания smae, как изначально в set, но это массив, а не набор, и массив никогда не изменяется. Используется только в ветке 2.

Выполнение с имитацией fs.readFile, которая занимает время следующим образом (имя файла и время в мс).

const timeTable = {
  "1": 600,
  "2": 500,
  "3": 400,
  "4": 300,
  "5": 200,
  "6": 100,
};

время запуска теста, подобное этому, показывает, что параллелизм работает --

[1]0--0.601
[2]0--0.502
[3]0.503--0.904
[4]0.608--0.908
[5]0.905--1.105
[6]0.905--1.005

Доступно в виде исполняемого файла в песочнице TypeScript Playground

avatar
João Pimentel Ferreira
9 января 2022 в 22:11
0

Это не использует асинхронность/ожидание, как запрошено OP, и работает только, если вы работаете в бэкенде с NodeJS. Хотя это все еще может быть полезно для некоторых людей, потому что пример, приведенный OP, заключается в чтении содержимого файла, и обычно вы выполняете чтение файла в бэкэнде.

Полностью асинхронный и неблокирующий:

const fs = require("fs")
const async = require("async")

const obj = {dev: "/dev.json", test: "/test.json", prod: "/prod.json"}
const configs = {}

async.forEachOf(obj, (value, key, callback) => {
    fs.readFile(__dirname + value, "utf8", (err, data) => {
        if (err) return callback(err)
        try {
            configs[key] = JSON.parse(data);
        } catch (e) {
            return callback(e)
        }
        callback()
    });
}, err => {
    if (err) console.error(err.message)
    // configs is now a map of JSON data
    doSomethingWith(configs)
})
Bergi
10 января 2022 в 00:12
0

OP никогда не просил не использовать async/await. Они заявляют: «Я пытаюсь перебрать массив файлов и await содержимое каждого файла.».

Bergi
10 января 2022 в 00:14
0

Кроме того, почему вы говорите, что require("async").forEach работает только в nodejs?

João Pimentel Ferreira
10 января 2022 в 11:17
0

@Bergi Я явно сказал, что OP не запросил именно это, и это просто работает с NodeJS. Хотя это все еще может быть полезно для некоторых людей, потому что пример, приведенный OP, заключается в чтении содержимого файла, и обычно вы выполняете чтение файла в бэкэнде.

Bergi
10 января 2022 в 11:23
0

О, я неправильно истолковал эту фразу как «использует (не использует async/await) в соответствии с запросом OP» вместо «не (использует async/await в соответствии с запросом OP)»

avatar
Souhail HARRATI
30 декабря 2021 в 16:02
1

Вот простое решение

const A = new Promise((resolve) => {
  setTimeout(resolve, 300, 'ankit');
});

const B = new Promise((resolve) => {
  setTimeout(resolve, 50, '123');
});

const C = new Promise((resolve) => {
  setTimeout(resolve, 500, '345');
});

const D = [A, B, C];
Promise.all(D).then(console.log);


avatar
sam
9 ноября 2021 в 16:34
9

@Bergi уже дал ответ о том, как правильно поступить в этом конкретном случае. Я не буду здесь дублировать.

Я хотел бы указать на разницу между использованием цикла forEach и for, когда речь идет о async и await

.

как работает forEach

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

Array.prototype.forEach = function (callback, thisArg) {
  if (this == null) { throw new TypeError('Array.prototype.forEach called on null or undefined'); }
  var T, k;
  var O = Object(this);
  var len = O.length >>> 0;
  if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function'); }
  if (arguments.length > 1) { T = thisArg; }
  k = 0;
  while (k < len) {
    var kValue;
    if (k in O) {
      kValue = O[k];
      callback.call(T, kValue, k, O); // pay attention to this line
    }
    k++;
  }
};

Вернемся к вашему коду, давайте извлечем обратный вызов как функцию.

async function callback(file){
  const contents = await fs.readFile(file, 'utf8')
  console.log(contents)
}

Итак, в основном callback возвращает промис, так как он объявлен с async. Внутри forEach callback просто вызывается обычным способом, если сам обратный вызов возвращает обещание, механизм javascript не будет ждать его разрешения или отклонения. Вместо этого он помещает promise в очередь заданий и продолжает выполнение цикла.

Как насчет await fs.readFile(file, 'utf8') внутри callback?

По сути, когда ваш асинхронный callback получает шанс быть выполненным, js-движок приостанавливается до тех пор, пока fs.readFile(file, 'utf8') не будет разрешен или отклонен, и возобновит выполнение асинхронной функции после выполнения. Таким образом, переменная contents хранит фактический результат fs.readFile, а не promise. Итак, console.log(contents) выводит из журнала содержимое файла, а не Promise

Почему for ... of работает?

, когда мы пишем общий цикл for of, мы получаем больше контроля, чем forEach. Выполним рефакторинг printFiles.

async function printFiles () {
  const files = await getFilePaths() // Assume this works fine

  for (const file of files) {
    const contents = await fs.readFile(file, 'utf8')
    console.log(contents)
    // or await callback(file)
  }
}

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

Выполнить последовательно

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

Вот пример:

const records = [1, 2, 3, 4];

async function saveRecord(record) {
  return new Promise((resolved, rejected) => {
    setTimeout(()=> {
      resolved(`record ${record} saved`)
    }, Math.random() * 500)
  });
}

async function forEachSaveRecords(records) {
  records.forEach(async (record) => {
    const res = await saveRecord(record);
    console.log(res);
  })
}

async function forofSaveRecords(records) {
  for (const record of records) {
    const res = await saveRecord(record);
    console.log(res);
  }
}
(async () => {
  console.log("=== for of save records ===")
  await forofSaveRecords(records)
  
  console.log("=== forEach save records ===")
  await forEachSaveRecords(records)
})()

Я использую setTimeout для имитации процесса сохранения записи в базу данных - он асинхронный и занимает произвольное время. При использовании forEach записи сохраняются в неопределенном порядке, но при использовании for..of они сохраняются последовательно.

avatar
Jatin Saradgikar
28 октября 2021 в 14:43
-3

Вы можете использовать цикл async.forEach из пакета async:

async.forEach(dataToLoop(array), async(data, cb) => {
                variable = await MongoQuery;
            }, function(err) {
                console.log(err);  
              })
            })
            .catch((err)=>{
              console.log(err);
            })
avatar
Matt Janssen
27 октября 2021 в 08:45
0

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

var urlsToGet = ['https://google.com', 'https://yahoo.com'];

fetchOneAtATime(urlsToGet);

function fetchOneAtATime(urls) {
    if (urls.length === 0) {
        return;
    }
    fetch(urls[0]).finally(() => fetchOneAtATime(urls.slice(1)));
}
Bergi
27 октября 2021 в 08:49
1

Лучше проверить urls.length перед вызовом .shift() в первый раз, и лучше использовать urls[0] и urls.slice(1) вместо очистки массива, который передается функции.

Bergi
27 октября 2021 в 08:50
1

Зачем использовать finally вместо then? Это будет игнорировать ошибки, в отличие от async/await.

Matt Janssen
27 октября 2021 в 12:37
0

Это было бы, если вы хотите выполнять каждую выборку, независимо от успеха предыдущих вызовов. Хорошая идея с пустой проверкой и без мутации массива! ✔

avatar
krupesh Anadkat
19 мая 2021 в 04:41
33

Картинка стоит 1000 слов — только для последовательного подхода


Общие сведения : Прошлой ночью я был в похожей ситуации. Я использовал асинхронную функцию в качестве аргумента foreach. Результат был непредсказуем. Когда я тестировал свой код 3 раза, он работал без проблем 2 раза и 1 раз не удалось. (что-то странное)

Наконец-то я собрался с мыслями и провел несколько тестов в блокноте.

Сценарий 1. Насколько непоследовательной может быть асинхронность в foreach

enter image description here

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  myPromiseArray.forEach(async (element, index) => {
    let result = await element;
    console.log(result);
  })

  console.log('After For Each Loop')
}

main();

Сценарий 2. Использование цикла for - of, предложенного @Bergi выше

enter image description here

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well
  for (const element of myPromiseArray) {
    let result = await element;
    console.log(result)
  }

  console.log('After For Each Loop')
}

main();

Если вы такой же олдскульный человек, как и я, вы можете просто использовать классический цикл for, он тоже работает :)

const getPromise = (time) => { 
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(`Promise resolved for ${time}s`)
    }, time)
  })
}

const main = async () => {
  const myPromiseArray = [getPromise(1000), getPromise(500), getPromise(3000)]
  console.log('Before For Each Loop')

  // AVOID USING THIS
  // myPromiseArray.forEach(async (element, index) => {
  //   let result = await element;
  //   console.log(result);
  // })

  // This works well too - the classic for loop :)
  for (let i = 0; i < myPromiseArray.length; i++) {
    const result = await myPromiseArray[i];
    console.log(result);
  }

  console.log('After For Each Loop')
}

main();

Надеюсь, это кому-нибудь поможет, добрый день, удачи!

krupesh Anadkat
19 мая 2021 в 04:48
4

Если кому-то интересно, что это за тема vscode — это официальная легкая тема github. & Если кто-то поранил глаза таким ярким снимком, приношу свои извинения 😅

avatar
Johnz
2 апреля 2021 в 13:18
8

Нехорошо вызывать асинхронный метод из цикла. Это связано с тем, что каждая итерация цикла будет отложена до завершения всей асинхронной операции. Это не очень эффективно. Это также сводит на нет преимущества распараллеливания async/await.

.

Лучшим решением было бы создать все промисы сразу, а затем получить доступ к результатам, используя Promise.all(). В противном случае каждая последующая операция не начнется, пока не завершится предыдущая.

Следовательно, код может быть реорганизован следующим образом;

const printFiles = async () => {
  const files = await getFilePaths();
  const results = [];
  files.forEach((file) => {
    results.push(fs.readFile(file, 'utf8'));
  });
  const contents = await Promise.all(results);
  console.log(contents);
}
Bergi
2 апреля 2021 в 15:46
9

Также нехорошо открывать тысячи файлов одновременно, чтобы читать их одновременно. Всегда нужно оценивать, какой подход лучше — последовательный, параллельный или смешанный. Последовательные циклы в принципе не плохи, await на самом деле делает их возможными в первую очередь. Кроме того, они не «утверждают преимущества» асинхронного выполнения, поскольку вы по-прежнему можете запускать несколько таких циклов одновременно (например, два одновременных вызова printFiles).

avatar
ChenZeTong
11 февраля 2021 в 01:45
3

Вот отличный пример использования асинхронности в цикле forEach.

Напишите свой собственный asyncForEach

async function asyncForEach(array, callback) {  
    for (let index = 0; index < array.length; index++) {
        await callback(array[index], index, array)
    }
}

Можно использовать так

await asyncForEach(array, async function(item,index,array){
     //await here
   }
)
avatar
yeah22
27 января 2021 в 21:57
10

Простым решением для замены неработающего цикла ожидания forEach() является замена forEach на map и добавление Promise.all( в начало.

Например:

await y.forEach(async (x) => {

до

await Promise.all(y.map(async (x) => {

В конце требуется дополнительный ).

srmark
7 октября 2021 в 11:06
0

Не совсем то же самое. Promise.all будет выполнять все промисы одновременно. Цикл for должен быть последовательным.

avatar
Adam Zerner
23 декабря 2020 в 19:51
2

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

Я думаю, важно решить, почему этого не происходит.

Подумайте о том, как работает forEach. Я не могу найти источник, но я предполагаю, что он работает примерно так:

const forEach = (arr, cb) => {
  for (let i = 0; i < arr.length; i++) {
    cb(arr[i]);
  }
};

Теперь подумайте о том, что происходит, когда вы делаете что-то вроде этого:

forEach(files, async logFile(file) {
  const contents = await fs.readFile(file, 'utf8');
  console.log(contents);
});

Внутри цикла forEach for мы вызываем cb(arr[i]), который в итоге оказывается logFile(file). Функция logFile имеет внутри себя await, поэтому, возможно, цикл for будет ждать этого await, прежде чем перейти к i++?

Нет, не будет. Как ни странно, await работает не так. Из документов:

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

Поэтому, если у вас есть следующее, номера не будут регистрироваться до "b":

const delay = (ms) => {
  return new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
};

const logNumbers = async () => {
  console.log(1);
  await delay(2000);
  console.log(2);
  await delay(2000);
  console.log(3);
};

const main = () => {
  console.log("a");
  logNumbers();
  console.log("b");
};

main();

Обратно к forEach, forEach похоже на main, а logFile похоже на logNumbers. main не остановится только потому, что logNumbers делает что-то await, а forEach не остановится только потому, что logFile что-то await делает.

avatar
Wojciech Maj
19 ноября 2020 в 10:45
-1

Если вы хотите перебирать все элементы одновременно:

async function asyncForEach(arr, fn) {
  await Promise.all(arr.map(fn));
}

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

Вариант A: Обещания

function asyncForEachStrict(arr, fn) {
  return new Promise((resolve) => {
    arr.reduce(
      (promise, cur, idx) => promise
        .then(() => fn(cur, idx, arr)),
      Promise.resolve(),
    ).then(() => resolve());
  });
}

Вариант Б: асинхронный/ожидание

async function asyncForEachStrict(arr, fn) {
  for (let idx = 0; idx < arr.length; idx += 1) {
    const cur = arr[idx];

    await fn(cur, idx, arr);
  }
}
Bergi
19 ноября 2020 в 15:02
0

Ваш вариант a включает в себя антишаблон конструктора Promise.

avatar
richytong
20 мая 2020 в 20:57
5

Вы можете использовать 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)
}
Mark Odey
29 мая 2020 в 19:03
1

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

avatar
Oliver Dixon
16 апреля 2020 в 17:18
17

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

В TypeScript:

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

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

await asyncForEach(receipts, async (eachItem) => {
    await ...
})
Ido Bleicher
14 октября 2021 в 08:10
0

Я думаю, будет полезно, если вы сможете завершить этот пример :) в разделе «Как использовать». В моем случае: await asyncForEach(configuration.groupNames, async (groupName) => { await AddUsersToGroup(configuration, groupName); })

avatar
lukaswilkeer
21 декабря 2019 в 01:11
6

Похож на ответ @Bergi, но с одним отличием.

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

ПС

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
}

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


* под побочным эффектом означает любой побочный эффект приложения, который может изменить состояние/поведение или вызвать ошибки в приложении, например ввод-вывод.

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

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

avatar
gsaandy
1 декабря 2019 в 16:59
7

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

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

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

avatar
PranavKAndro
24 ноября 2019 в 20:31
5

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

// 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);
Bergi
24 ноября 2019 в 21:48
0

Ваши методы 1 и 2 являются просто неправильными реализациями, где должно было использоваться Promise.all - они не учитывают ни один из многих крайних случаев.

PranavKAndro
25 ноября 2019 в 14:15
0

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

Bergi
25 ноября 2019 в 15:25
0

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

PranavKAndro
26 ноября 2019 в 05:57
0

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

Bergi
26 ноября 2019 в 13:22
0

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

PranavKAndro
26 ноября 2019 в 17:22
0

Конус на Берги, дело не в том, что лучше, а в различных доступных решениях. Вы упомянули обработку ошибок как пустой массив, обработка ошибок пустого массива выполняется внутри forEach. Давайте опустим этот разговор здесь. Опять же, это все возможные способы достижения

Bergi
26 ноября 2019 в 17:26
0

Дело не в том, что «хорошо» или «лучше» из пула допустимых решений, а в том, что ваш код полностью сломан. Вы пробовали, что происходит, когда вы передаете пустой массив? Он висит навсегда. Вы пробовали, что происходит, когда someAPICall() отклоняется? Вы получаете необработанный отказ от обещания, и он зависает навсегда.

PranavKAndro
27 ноября 2019 в 05:39
0

@Bergi: Обычно, если какой-либо разработчик использует асинхронное ожидание для ожидания обещания, оно должно быть окружено попыткой улова. someAPICall() не является оболочкой, которую я предоставляю. Это функция разработчика, которую разработчик пишет внутри обратного вызова, который я предоставляю как часть оболочки, разработчик должен окружить это с помощью try catch. Если вы хотите, я могу обновить ответ, обрабатывается пустой массив, вы запускаете и дайте мне знать. Он работает идеально.

Bergi
27 ноября 2019 в 09:25
0

«разработчик должен окружить это с помощью try catch» — а вы этого не сделали. Если вы думаете, что это всегда нужно делать, то да, пожалуйста, по крайней мере, сделайте это сами. Но нет, обычно ожидается, что обещание, возвращаемое forEachAsync(…), должно быть отклонено. И нет, ваш код не обрабатывает пустые массивы, вы пробовали? await [].forEachAsync(() => {}); console.log("never happens");

avatar
jgmjgm
14 октября 2019 в 18:35
4

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

avatar
master_dodo
26 мая 2019 в 22:08
10

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

TypeError [ERR_INVALID_CALLBACK]: Callback must be a function
avatar
Beau
12 марта 2019 в 23:31
5

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

// 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, доступный для любых массивов, определенных после этих операций to.

Давайте проверим...

// 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>
avatar
Scott Rudiger
21 июня 2018 в 16:55
0

Аналогично p-итерации Антонио Вэла, альтернативный модуль 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

avatar
Francisco Mateo
15 июня 2018 в 11:17
423

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

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

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

См. спецификацию: proposal-async-iteration


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

Francisco Mateo
8 сентября 2018 в 14:13
0

Не уверен, где определяется file. Это портировано 1: 1 из вопроса выше.

Yevhenii Herasymchuk
8 января 2019 в 16:27
33

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

Antonio Val
9 января 2019 в 10:30
5

Я не думаю, что этот ответ касается первоначального вопроса. for-await-of с синхронным итерируемым объектом (в нашем случае массивом) не охватывает случай одновременной итерации массива с использованием асинхронных операций на каждой итерации. Если я не ошибаюсь, использование for-await-of с синхронным итерируемым значением для значений без обещаний аналогично использованию простого for-of.

Vadim Shvetsov
17 января 2019 в 13:34
2

Как мы делегируем массив files в fs.readFile здесь? Берется из iterable?

Rafi Henig
11 сентября 2019 в 01:07
1

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

jib
18 февраля 2021 в 13:52
3

Этот ответ имеет ту же проблему, что и ОП: он обращается ко всем файлам параллельно. Серийная печать результатов просто скрывает это.

Ray Foss
27 апреля 2021 в 05:17
0

Основная проблема с этим ответом заключается в том, что в for await при использовании итератора синхронизации, такого как массив, карта должна возвращать обещание. В узле fs по умолчанию не является асинхронным... использование fsPromises позволит правильно выполнять это параллельно. Я думаю, хорошо было бы привлечь внимание к этому превосходному, хорошо поддерживаемому синтаксису.

Lars Wissler
14 августа 2021 в 16:40
0

Меньше символов не означает, что это проще. В основном это запутанно и нечитабельно.

Jesus Loves You
10 ноября 2021 в 13:06
0

лучший ответ.....

Bergi
13 декабря 2021 в 15:57
1

Этот ответ неверен. files.map() возвращает массив промисов, не асинхронный итератор, для которого был сделан for await! Это приведет к необработанным отказам!

avatar
Timothy Zorn
26 марта 2018 в 19:48
141

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

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

parrker9
28 марта 2018 в 20:48
1

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

GollyJer
9 июня 2018 в 00:24
2

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

Timothy Zorn
17 июня 2018 в 15:00
4

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

Shay Yzhakov
30 мая 2019 в 12:54
0

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

Timothy Zorn
30 мая 2019 в 16:51
2

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

user7075574
30 января 2020 в 02:19
0

Есть ли способ сделать это, по-видимому, быстрым, когда у вас есть страница Express, для которой требуется список ресурсов для отображения...?

Timothy Zorn
31 января 2020 в 16:03
3

Если вам нужно, чтобы асинхронные процессы завершились как можно быстрее, и вас не волнует их последовательное завершение, попробуйте одно из предложенных решений с большим количеством голосов, которое использует Promise.all. Пример: Promise.all(files.map(async (file) => { /* code */ }));

Remi Mélisson
18 марта 2021 в 14:41
0

мой любимый вариант, я бы хотел увидеть нативный Promise.chain() вроде all()

Bigyan Devkota
29 июля 2021 в 21:44
0

Это отлично работает -- спасибо, здесь важным шагом является обещание ожидания; вот что значит тяжелая нагрузка

avatar
Matt
22 марта 2018 в 15:11
40

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

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

Damien Romito
2 июня 2021 в 15:32
0

использование: ожидайте myArray. forEachAsyncParallel( async (item) => { await myAsyncFunction(item) })

avatar
Babakness
28 февраля 2018 в 04:41
4

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

avatar
chharvey
23 февраля 2018 в 00:47
14

В дополнение к ответу @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 консоль может регистрировать содержимое файлов в порядке их чтения. Например, если очень маленький файл завершает чтение перед очень большим файлом, он будет зарегистрирован первым, даже если маленький файл будет после<17832225660227> большого файла в массиве files. Однако в моем методе выше вы гарантированно будете регистрировать файлы в том же порядке, что и предоставленный массив.

avatar
Zachary Ryan Smith
4 февраля 2018 в 16:03
-1

Я бы использовал хорошо проверенные (миллионы загрузок в неделю) модули 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
```
jbustamovej
20 февраля 2018 в 06:24
1

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

Zachary Ryan Smith
21 февраля 2018 в 01:54
0

как вы можете видеть здесь, я заинтересован и готов использовать async/await вместо async lib. Сейчас я думаю, что каждому свое время и место. Я не уверен, что async lib == «ад обратных вызовов» и async/await == «современная эра JS». imo, когда async lib> async/await: 1. сложный поток (например, очередь, груз, даже автоматический, когда все усложняется) 2. параллелизм 3. поддержка массивов/объектов/итераций 4. обработка ошибок

avatar
LeOn - Han Li
24 сентября 2017 в 20:00
7

Одно важное предостережение: метод 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, чтобы помочь, но в основном использовать тот же самый базовый цикл for.

Array.prototype.forEachAsync = async function(cb){
    for(let x of this){
        await cb(x);
    }
}
Bergi
16 ноября 2017 в 13:57
0

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

Timothy Zorn
26 марта 2018 в 19:54
0

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

avatar
Jay Edwards
22 сентября 2017 в 23:03
11

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

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));
}
Jay Edwards
26 сентября 2017 в 09:08
3

Небольшое дополнение, не забудьте обернуть ваш await/async в блоки try/catch!!

avatar
Hooman Askari
26 августа 2017 в 10:47
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)))
avatar
Antonio Val
10 июля 2017 в 08:15
47

Модуль 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);
  });
})();