[Перевод] Async/await это шаг назад для JavaScript'a?

2b09af520eef4f3087dcea5282a6a44a.png

В конце 2015 года я услышал об этой паре ключевых слов, которые ворвались в мир JavaScript, чтобы спасти нас от promise chain hell, который, в свою очередь, должен был спасти нас от callback hell. Давайте посмотрим несколько примеров, чтобы понять, как мы дошли до async/await.

Допустим, мы работаем над нашим API и должны отвечать на запросы серией асинхронных операций:
 — проверить валидность пользователя
 — собрать данные из базы данных
 — получить данные от внешнего сервиса
 — изменить и записать данные обратно в базу данных

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

function handleRequestCallbacks(req, res) {
  var user = req.user
  isUserValid(user, function (err) {
    if (err) {
      res.error('An error ocurred!')
      return
    }
    getUserData(user, function (err, data) {
      if (err) {
        res.error('An error ocurred!')
        return
      }
      getRate('service', function (err, rate) {
        if (err) {
          res.error('An error ocurred!')
          return
        }
        const newData = updateData(data, rate)
        updateUserData(user, newData, function (err, savedData) {
          if (err) {
            res.error('An error ocurred!')
            return
          }
          res.send(savedData)
        })
      })
    })
  })
}

И это так называемый callback hell. Теперь вы знакомы с ним. Все его ненавидят, так как его трудно читать, отлаживать, изменять, он уходит все глубже и глубже во вложенности, обработка ошибок повторяется на каждому уровне и т.д.
Мы могли бы использовать знаменитую async библиотеку, чтобы немного очистить код. Код стал бы лучше, так как обработка ошибок, по крайней мере была бы в одном месте:
function handleRequestAsync(req, res) {
  var user = req.user
  async.waterfall([
    async.apply(isUserValid, user),
    async.apply(async.parallel, {
      data: async.apply(getUserData, user),
      rate: async.apply(getRate, 'service')
    }),
    function (results, callback) {
      const newData = updateData(results.data, results.rate)
      updateUserData(user, newData, callback)
    }
  ], function (err, data) {
    if (err) {
      res.error('An error ocurred!')
      return
    }
    res.send(data)
  })
}

Позже мы узнали как использовать промисы и подумали, что мир больше не злится на нас, и мы почувствовали, что нужно провести рефакторинг кода еще раз, ведь все больше и больше библиотек также движется в мир промисов.
function handleRequestPromises(req, res) {
  var user = req.user
  isUserValidAsync(user).then(function () {
    return Promise.all([
      getUserDataAsync(user),
      getRateAsync('service')
    ])
  }).then(function (results) {
    const newData = updateData(results[0], results[1])
    return updateUserDataAsync(user, newData)
  }).then(function (data) {
    res.send(data)
  }).catch(function () {
    res.error('An error ocurred!')
  })
}

Это гораздо лучше, чем раньше, гораздо короче и намного чище! Тем не менее возникло слишком много накладных расходов в виде множества, then () вызовов, function () {…} блоков и необходимости добавлять несколько операторов return повсюду.

И наконец мы слышим о ES6, обо всех этих новых вещах, которые пришли в JavaScript, такие как стрелочные функции (и немного деструктуризации только, чтобы было немного веселее). Мы решаем дать нашему прекрасному коду еще один шанс.

function handleRequestArrows(req, res) {
  const { user } = req
  isUserValidAsync(user)
    .then(() => Promise.all([getUserDataAsync(user), getRateAsync('service')]))
    .then(([data, rate]) => updateUserDataAsync(user, updateData(data, rate)))
    .then(data => res.send(data))
    .catch(() => res.error('An error ocurred!'))
}

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

И приходит async/await


Несколько месяцев спустя, async/await выходит на сцену. Он собирался попасть в спецификацию ES7, затем идею отложили, но т.к. есть Babel, мы прыгнули в поезд. Мы узнали, что можем отметить функцию как асинхронную и что это ключевое слово позволит нам внутри функции «остановить» ее поток исполнения до тех пор, пока промис не решит, что ваш код снова выглядит синхронным. Кроме того, функция async всегда будет возвращать промис, и мы можем использовать try/catch блоки для обработки ошибок.

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

async function asyncHandleRequest(req, res) {
  try {
    const { user } = req
    await isUserValidAsync(user)
    const [data, rate] = await Promise.all([getUserDataAsync(user), getRateAsync('service')])
    const savedData = await updateUserDataAsync(user, updateData(data, rate))
    res.send(savedData)
  } catch (err) {
    res.error('An error ocurred!')
  }
}

И теперь код снова выглядел как старый обычный императивный синхронный код. Жизнь продолжилась как обычно, но что-то глубоко в вашей голове говорит нам что что-то здесь не так…

Функциональная парадигма программирования


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

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

Но… постойте!!!


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

И мы вдруг понимаем, почему код на основе async/await смотрелся странно. Ведь мы писали обычный императивный код, как в 80-х годах, обрабатывали ошибки с try/catch, как в 90-х годах, управляли внутренним состоянием и переменными, делая асинхронные операции с помощью кода, который выглядит как синхронный, но который внезапно останавливается, а затем автоматически продолжает выполнение, когда асинхронная операция будет завершена (когнитивный диссонанс?).

Последние мысли


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

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

См. также


Состав и функциональное программирование, Брайан Лонсдорф.
Еще одно мнение о том, что async/await не такая уж хорошая вещь.

Комментарии (3)

  • 24 января 2017 в 13:35

    +1

    Так и не понял аргументов автора. Судь async/await как раз в том, чтобы сделать асинхронный код максимально похожим на обычный, чтобы его было проще понимать и легче отлаживать.
    Мы не определяли промежуточные переменные для хранения этого состояния
    Иногда это бывает очень нужно, например для такого случая:
    const a = await getA();
    const b = await getB(a);
    const c = await getC(a, b);
    

    Аналогичный код с promise’ами выглядит очень гадко.
    • 24 января 2017 в 14:25

      0

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

      У меня async/await был самой ожидаемой фичей ES-Next, пока не пришлось столкнуться с Observables:) Теперь async/await, в общем-то, не у дел, но это и не значит, что он плох.

      Ну и, к слову, несмотря на то, что код выглядит как синхронный, он именно что только «выглядит» так — асинхронность все равно надо держать в уме, и, поскольку await разворачивается в Promise, все равно нужно понимать, как именно он это делает.

  • 24 января 2017 в 13:46

    0

    У нас, так исторически сложилось, что тесты написаны асинхронно. Правда на Scala, но сути это не меняет.
    При этом используются целых три стиля — flatMap (на сколько я понимаю, это в js называется «через callback»), с помощью for (в js аналога, по моему, нет, похоже на do в Haskell), и async/await.
    Для меня с flatMap единственное неудобство — большой уровень вложенности синтаксических конструкций (соответственно проблема, как поступать с отступами). for выглядит чище, я последнее время склоняюсь к нему. Стиль async/await имеет много фанатов, но мне от показался малопонятным (я давно программирую на функциональных языках и более императивный код мне читать сложнее). Кроме того, в этом коде очень легко где-нибудь забыть написать await на какую-нибудь фьючу, результат которой игнорируется (в нашем случае это обычно было удаление созданных для теста сущностей), и получить race condition.
    В общем, async/await мне не понравился.

© Habrahabr.ru