Асинхронная итерация в JavaScript

С выходом восьмой версии Node в LTS на улице node-разработчиков наступил натуральный праздник; ещё бы, async/await теперь смело можно использовать в продакшене. Конечно, с собой async/await принесли невероятно много хорошего, но кое-что, казалось бы, очевидное, всё же следует отметить.

Я говорю про использование асинхронного кода с итерацией. Вот пример, с которым я сталкивался:

const results = items.map(async item => {
 const result = await updateItem(item.id) // some async method
 return result
})

Получим ли мы массив результатов? Нет. Вместо этого мы получим массив промисов. Ещё пример, тоже из личного опыта:

const items = await getItems()

items.forEach(async item => {
  const newOptions = {
    some: 'options'
  }
  await updateItem(newOptions) // some async method
})

const newItems = await getItems()

Получим ли мы массив обновлённых записей? Снова нет. Что мы получим — предугадать сложно, какие-то записи будут обновлены, какие-то нет.

Почему так происходит?

Ошибка первого примера — асинхронная функция, объявленная с ключевым словом async всегда возвращает промис, даже если внутри неё только синхронный код. Зная это, можно переписать первый пример следующим образом:

const results = await Promise.all(items.map(async item => {
  const result = await updateItem(item.id) // some async method
  return result
}))

Хотя в конкретно этой ситуации можно обойтись вообще без async/await внутри map:

const results = await Promise.all(items.map(item => updateItem(item.id)))

Ошибка второго примера — отсутствие контроля. Здесь создается набор промисов, которые неизвестно когда выполнятся, в данном случае await внутри forEach ничем не поможет. В этой ситуации я предлагаю два решения. Первое — свести всё к решению варианта №1, заменив forEach на map и проконтролировав выполнение промисов с помощью Promise.all.

Второе — использовать for .. in/of. Понятно, что в наш просвещённый век никто уже не пишет for, все используют map/reduce но как ни странно, с приходом async/await итерация с помощью for получила свой второе рождение. Смотрите сами, ранее чтобы выполнить массив промисов последовательно приходилось изрядно изгаляться, например, с reduce:

const finalPromise = items.reduce((checkpoint, item) => {
  return checkpoint()
    .then(_ => asyncMethod(item))
  })
}, Promise.resolve())

Решение не самое изящное, но что поделать. С async/await всё выглядит куда как проще:

for (let item of items) {
  await asyncMethod(item)
  await otherAsyncMethod()
}

Всё, асинхронные вызовы исполняются строго последовательно, один за одним. Не всегда это нужно, но когда нужно, это, на мой взгляд, намного более изящное решение, чем использование reduce.

Резюмируя, хочу сказать, что async/await вещь, несомненно, полезная, но с ней, так же как и со многими другими аспектами JavaScript, легко попасть впросак. Будьте внимательны и не повторяйте моих ошибок.

comments powered by Disqus