Howtojs

Как использовать async/await в цикле forEach?

9 февраля 2021 г. • ☕️ 4 мин.

Проблема

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

const fs = require("fs/promises")

async function main() {
  const dir = await fs.readdir("./")

  const files = []

  dir.forEach(async name => {    const file = await fs.readFile(name)
    files.push(file)
  })

  console.log(files) // []
}

main()

Как устроен array.forEach?

forEach является синхронной функций и не умеет ждать завершения асинхронной работы в callback.

Как устроена async функция?

Мы передаем асинхронную (async) функцию, как (callback) в forEach. Внутри этой функции, мы можем использовать ключевое слово await. При вызове, асинхронная фунция возвращает Promise, который надо подождать с помощью ключевого слова await либо функции .then.

Что же здесь происходит?

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

Как добиться результата?

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

async function main() {
  const dir = await fs.readdir("./")

  const files = []

  for (const name of dir) {    const file = await fs.readFile(name)
    files.push(file)
  }

  console.log(files)
}

При последовательном выполнении используется цикл for...of. Он не требует передачи callback и весь код исполняется в основном логическом потоке, поэтому await будет работать, как и ожидается.

Классический цикл for или while будут тоже работать.

Одновременное (concurrent) выполнение

async function main() {
  const dir = await fs.readdir("./")

  const files = await Promise.all(dir.map(async name => fs.readFile(name)))
  console.log(files)
}

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

  • функция array.map, которая возвращает массив из Promise
  • функция Promise.all, которая позволяет подождать завершения массива из Promise

Этот вариант похож на первый с forEach, где мы запускали несколько асинхронных операций одновременно, только здесь, мы явно ждем их завершения.

Заключение

Асинхронный код всегда требует того, чтобы мы дождались результата в явном виде. Можно легко упустить из виду асинхронную операцию, которая “отсоединится” от основного логического потока программы.

При проблемах в понимании асинхронного кода, всегда стоит задавать себе вопрос: Что я сделал для того, чтобы дождаться здесь результата?