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

Первое что хочется развеять, это распространенное заблуждение о том, что async/await — это фича ES7.

По моему мнению, использование терминов ES6 и ES7 само по себе не очень верное и может ввести разработчиков в заблуждение. После удачного релиза спецификации ES2015, называемой ES6, у многих людей сложилось ошибочное мнение, что все в нее не вошло и заполифилено через babel — это фичи ES7. Это не так. Вот список того что появится с релизом спецификации ES2016. Как видите он не такой большой и async/await в нем никак не значится.

Я хочу, чтобы мы говорили правильно. И говоря о той, или иной фиче, ссылались на конкретную спецификацию в рамках которой она описана и реализована, а не мифические ES6, ES7 … ESN.

Двигаемся дальше. Так что же такое async/await простыми словами?


Говоря общедоступным языком async/await?—?это Promise.

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

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

Давайте посмотрим, как же выглядит наш единорог и разберемся как он работает.


Вот простой пример асинхронного Redux экшена для выхода из кабинета:

export function logout(router) {
  return async (dispatch) => {
    try {
      const {data: {success, message}} = await axios.get('/logout');
 
      (success)
        ? dispatch({ type: LOGOUT_SUCCESS })
        : dispatch({ type: LOGOUT_FAILURE, message });
 
     } catch (e) {
         dispatch({ type: LOGOUT_FAILURE, e.data.message });
     }
   };
}

А теперь идем от общего к частному


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

Что нужно сделать чтобы начать работу?


Если не использовать никакой системы сборки, то достаточно установить babel и babel-runtime.

babel test.js -o test-compile.js —optional runtime —experimental

В остальных случаях, лучше смотреть настройки исходя их системы сборки и версии babel. Это очень важно, так как настройки в версии babel5 и babel6 сильно различаются.

Как создается асинхронная функция?


async function unicorn() {
  let rainbow = await getRainbow();
  return rainbow.data.colors
}

Создание асинхронной функции состоит из двух основных частей:

1. Использования слова async перед объявлением функции.

Как мы видим из примера c logout(), это так же работает при использовании стрелочных функций. Еще это работает для функций классов и статичных функций. В последнем случае async пишется после static.

2. В теле самой функции мы должны использовать слово await.

Использование слова await сигнализирует о том, что бы основной код ждал и не возвращал ответ, пока не выполниться какое-то действие. Оно просто обрабатывает Promise для нас и ждет пока он вернет resolve или reject. Таким образом, создается впечатление, что код выполняется синхронно.

* Для работы с await функция должна быть асинхронной и объявлена с помощью ключевого слова async. В противном случае это просто не будет работать.

Как работает await и какую функцию выполняет?


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

Хорошо, если await это аналог .then() у Promise, как же мне тогда поймать и обработать исключения?


async function unicorn() {
  try {
    let rainbow = await getRainbow();
    return rainbow.data.colors;
  } catch(e) {
    return {
      message: e.data.message,
      somaText: ‘Текст о не легкой жизни единорогов’
    }
  }
}

Так как код в синхронном стиле, по этой причине мы можем использовать старый добрый try/catch для решения подобных задач.

Дополнительно хочется акцентировать на этом внимание.



Использование try/catch это единственный способ поймать и обработать ошибку. Если по каким-то причинам вы решите его не использовать или просто забыли, это может привести к отсутствию возможности обработки, а так же потере вовсе.

В какой момент происходит выполнение кода следующего за await?


async function unicorn() {
  let _colors = [];
  let rainbow = await getRainbow();
  
  if(rainbow.data.colors.length) {
     _colors = rainbow.colors.map((color) => color.toUpperCase());
  }
  
  return _colors;
}

Код следующий после await, продолжает свое выполнение только тогда когда функция используемая с await вернет resolve или reject.

Что если функция используемая с await не возвращает Promise?


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

Что если объявить функцию асинхронной, но не использовать await?


async function unicorn() {
  let rainbow = getRainbow();
  return rainbow;
}

В таком случае, на выходе мы получим просто ссылку на Promise функции getRainbow().

Что будет если я напишу несколько функций использующих await подряд?


async function unicorn() {
   let rainbow = await getRainbow();
   let food = await getFood();
   return {rainbow, food}
}

Такой код будет выполняться последовательно. Сначала отработает getRainbow(), после того как она вернет resolve или reject начнет работать getFood(). Один вызов, один результат.

А если мне нужно одновременно получить результат от нескольких вызовов?


async function unicorn() {
  let [rainbow, food] = await Promise.all([getRainbow(), getFood()]);
  return {rainbow, food}
}

Так как мы уже разобрались, что мы имеем дело с Promise. Следовательно можно использовать метод .all() объекта Promise для решения такого рода задач.

Дополнительно хочу заметить, что конструкция await * arrayOfPromises больше не актуальна и удалена из спецификации. При попытке ее использовать вы по получите сообщение с любовью о том, что лучше использовать Promise.all().

Пример сообщения:
  await* has been removed from the async functions proposal. Use Promise.all()

Обновил информацию по конструкции await*. Спасибо xGromMx и degorov.

Что еще хорошо бы знать для успешной работы?


async function getAllUnicorns(names) {
  return await Promise.all(names.map(async function(name) {
    var unicorn = await getUnicorn(name);
    return unicorn;
  }));
}

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

Вроде бы все.

С основными теоретическими аспектами мы разобрались, если что-то осталось непонятным или у Вас есть что дополнить, жду в комментариях.

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

Выводы:


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

Но это на первый взгляд.

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

Почти все вечно зеленые браузеры, из коробки, на 93%-98% поддерживают фичи ES2015 (таблица). Для меня это означает, что начиная новый проект, исходя из требований и стека, я уже задумаюсь об необходимости babel на проекте.

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

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

И последнее.

Большинство задач с использованием async/await прекрасно решаются с помощью генераторов.

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

Итого


Отвечаю себе на вопрос заданный в самом начале:
Если я и буду использовать асинхронные функции, то только на своих Pet проектах и только для написания API.

Например мне понравилось использовать их в экшенах для Redux. Выглядит все красиво и гармонично.

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

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

Всем спасибо за внимание. Удачи!

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


  1. xGromMx
    26.04.2016 13:47
    +3

    Синтаксиса await* не будет так как они говорят что есть уже Promise.all. Это не одно и тоже что в yield и yiled*


    1. shanhaichik
      26.04.2016 14:29

      Привет, xGromMx. А можно ознакомиться где это написано или сказано? Заранее спасибо.


      1. degorov
        26.04.2016 14:46

        Babel говорит:
        repl: await* has been removed from the async functions proposal. Use Promise.all() instead. (2:24)

        Ну а так где-то тут, наверное…


        1. shanhaichik
          26.04.2016 14:48

          Спасибо, degorov. Ознакомлюсь и внесу соответствующие корректировки.


      1. xGromMx
        26.04.2016 15:26

        Глянь это ишью https://github.com/tc39/ecmascript-asyncawait/issues/85


  1. arusakov
    26.04.2016 15:17

    А как сейчас можно отказаться от Babel, если нужна минификация для продакшена? Я недавно думал отключить Babel на одном проекте, но потом понял, что Uglifyjs не поддерживает нормально ES6, и оставил все, как есть.


    1. vintage
      26.04.2016 15:45
      -5

      Попробуйте меньше копипастить и минификация вам не понадобится ;-)


      1. shanhaichik
        26.04.2016 15:56
        -1

        vintage, вы злодей :)


    1. shanhaichik
      26.04.2016 15:55
      +1

      arusakov, Я понимаю что с места этот барьер будет тяжело взять. Я к этому не призываю. И сам недавно столкнулся с похожей проблемой.
      Но на мой взгляд это не повод опускать руки. Скорость с которой браузеры вводят поддержку новых фич, не говоря уже о ES2015, заставит разработчиков, подобного рода инструментов, подтянуть свои продукты под современные реалии. Иначе придет кто-то новый, более адаптированный.
      Например у Uglifyjs есть экспериментальная ветка harmony, как раз нацеленная на поддержку ES6.

      В данный момент, я решил пробовать разработку без babel на небольших внутренних проектах. Где можно принебречь некоторыми вещами, например Uglifyjs.


      1. xGromMx
        26.04.2016 16:59

        Частично с минификацией справляется closure compiler


  1. radist2s
    26.04.2016 16:20
    -2

    Исходя из того, что компилирует babel, await перестает быть асинхронным и поток вполне себе блокируется. Так что это то еще зло, если не используется в отдельном воркере.


    1. f0rk
      26.04.2016 17:38

      Где вы там блокировку увидели? while(1) в regeneratorRuntime используется для того чтоб перезапускать генераторы с циклами, ничего он не блокирует.


    1. staticlab
      26.04.2016 17:54

      Там вся суть в функции _asyncToGenerator(), в которую передаётся генератор.


    1. Yozi
      26.04.2016 17:54

      Эм, пруфы, пожалуйста. Babel переводит async/await либо в генераторы, либо использует регенератор.


      1. radist2s
        26.04.2016 17:56
        +2

        Да-да, теперь понял, ошибался.


  1. Scf
    26.04.2016 16:53

    Для меня это выглядит так, как будто async/await бесполезен в реальных проектах. Допустим, у нас есть 3 функции:

    function authenticateUser(login, password) {} //возвращает Promise<userId> при успехе
    function getUserDetails(userId) {}
    function getUserAvatar(userId) {}
    

    И я хочу запустить authenticateUser, а по его завершению — getUserDetails и getUserAvatar параллельно.
    Через `then` это делается элементарно, включая обработку ошибок — достаточно проверить результирующий промис:
    let detailsAndAvatar = authenticateUser(login, password).then(function(userId) {
        return Promise.all(getUserDetails(userId), getUserAvatar(userId));
    });
    

    Как это будет выглядеть на async/await?


    1. xGromMx
      26.04.2016 17:02
      +5

      async f() {
        const userId = await authenticateUser(login, password);
        return await Promise.all([getUserDetails(userId), getUserAvatar(userId)]);
      }
      


      1. Scf
        26.04.2016 17:50

        Красота) А можно пример с тремя уровнями вложенности? Я никак не могу сообразить. Т.е.

        f    -> f1
                 |-> f11
                 |-> f12
             -> f2
                 |-> f21
                 |-> f22
        
        


        1. f0rk
          26.04.2016 18:07

          Диаграммка не очень понятная, приведите пример с промисами.


      1. keksmen
        26.04.2016 17:54
        +3

        Простите за снобство, но это плохой пример. Если async f() возвращает обещание, то зачем оператор await после return? Не достаточно ли будет "вернуть" обещание? Механизм Promise'ов ждёт обещания любой глубины. Даже если обещание resolve'ит другое обещание.


        1. xGromMx
          26.04.2016 18:19
          +1

          это и есть проблемы промисов у них map и flatMap ведут себя одинаково хотя было бы правильно с точки зрения функтора и монад так

          const pf = Promise.of(42).map(v => v + 10);
          const pm = Promise.of(42).flatMap(v => Promise.of(v + 10).delay(1000)) // псевдокод с delay
          


        1. xGromMx
          27.04.2016 00:32
          +1

          За что минусы?


          1. vitalets
            27.04.2016 11:21

            Promise.of нет в спецификации


            1. faiwer
              27.04.2016 11:22
              +2

              А xGromMx и не утверждал обратного. Он просто привёл пример, того как, по его мнению, было бы правильнее. Код напоминает scala.


    1. f0rk
      26.04.2016 17:03

      опоздал :)


      1. xGromMx
        26.04.2016 17:03

        где обертка в async?)


    1. xGromMx
      26.04.2016 17:03
      -1

      Грубо говоря async/await есть do монада


    1. vintage
      27.04.2016 11:28
      +1

      На сопрограммах (node-fibers) это будет выглядеть так:


      let userId = authenticateUser( login , password ).wait()
      let detailsFuture = getUserDetails( userId )
      let avatarFuture = getUserAvatar( userId )
      let detailsAndAvatar = [ detailsFuture.wait() , avatarFuture.wait() ]


      1. vintage
        27.04.2016 11:33

        А с моим велосипедом, вообще вот так:


        let userId = authenticateUser( login , password )
        let details = getUserDetails( userId )
        let avatar = getUserAvatar( userId )
        let detailsAndAvatar = [ details , avatar ]


        1. faiwer
          27.04.2016 12:10

          Вы какую-то слишком общую ссылку привели. Лучше сразу на примеры с велосипедом. По какому принципу это работает? В каком контексте? Нужно ли оборачивать эти методы чем-нибудь? Нужно ли на вершине стека какой-нибудь Fiber(context) запускать?


          А самое интересное, чего я пока не понял до конца, это в чём различия подхода волокон и async-await? Я так понимаю, и там и там, не создаётся новых потоков, а только переключаются стеки состояний, что дешевле, чем, скажем, новые потоки, но тем не менее далеко не бесплатно. Или нет? Вы не могли бы объяснить в двух словах?


          1. vintage
            27.04.2016 12:40

            Ок, вот более конкретная ссылка. Да, всё приложение нужно стартовать в волокне и всё.


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


            1. xGromMx
              27.04.2016 13:17

              Скинь ссылку на статью про волокна, я помню у тебя где-то было на github


              1. vintage
                27.04.2016 13:26

                Речь об этой статье? Там фактические не волокна, а эмуляция async-await через генераторы.


                1. xGromMx
                  27.04.2016 15:04

                  да о ней


  1. GeraldIstar
    27.04.2016 07:55
    +1

    То есть, если я все правильно понимаю — отловить ошибки в async/await функциях можно только через try/catch? Учитывая не очень хорошую производительность try/catch, мне кажется что это огромный минус таких функций.
    Кстати, в redux-saga используются генераторы, но на первый взгляд работают точно таким же образом (я не говорю про то как там внутри реализовано).


    1. shanhaichik
      27.04.2016 07:59
      +2

      GeraldIstar, да, все верно. Не очень хороша производительность, а так же V8 не оптимизирует функции, содержащие эту конструкцию


    1. faiwer
      27.04.2016 08:25

      А разве Promise-ы в целом не работают через try-catch?


      new Promise((r,j) => { throw 1; }).catch(err => console.log('error=', err))
      // error= 1

      Т.е. это скорее общая особенность работы Promise, нежели особенность await. Проигрываем в оптимизации, зато получаем проброс ошибок.


      1. Shannon
        27.04.2016 10:02

        Видимо нет, потому что в хроме:

        function testFunction() {
            new Promise((r,j) => { throw 1; }).catch(err => console.log('error=', err))
        }
        

        Function is optimized
        

        Добавление в любом месте try-catch (даже пустого) приводит к:
        Function is not optimized
        


        1. faiwer
          27.04.2016 11:20

          У вас testFunction is optimized, а внутри исходников promise, где-нибудь есть метод, внутри которого стоит try-catch. И вот там будет is not optimized. Полагаю, что async-методы в первое время будут, что с try-catch, что без него, не оптимизированными, а уже апосля руки дойдут и до них. Во всяком случае, смотря на список убийц оптимизации у меня сложилось впечатление, что нужно писать код стоя на одной ноге на ципочках, чтобы не выпасть из оптимизации. Шаг в лево, шаг в право — приехали.


          1. Shannon
            27.04.2016 12:46
            +1

            Вполне может быть, в реализации промисов Bluebird от Petka Antonov, которые быстрее нативных, как раз используется изолированная функция tryCatch для catch, использование которой не мешает V8 оптимизировать весь остальной ваш код

            Для теоретического async-await пока вариант явно отказаться либо от ловли ошибок, либо от оптимизаций (интересно как с этим дела в Edge). Может потому и не спешат внедрять

            Кстати, в V8 уже оптимизировали некоторые моменты из числа убийц:

            • функции, содержащие выражение for-of;
            • функции, содержащие составной оператор присваивания let;
            • функции, содержащие составной оператор присваивания const;
            • функции, содержащие объектные литералы, которые, в свою очередь, содержат объявления __proto__, get или set.
            • бесконечные циклы
            • 5.1. Ключ не является локальной переменной (первый пример с nonLocalKey1)
            • частично 5.2. Итерируемый объект не является «простым перечисляемым»
            • может что-то еще, протестировал только эти

            Function is optimized by TurboFan
            

            И вполне возможно скоро можно будет не так активно задумываться над убийцами оптимизаций


  1. iKBAHT
    27.04.2016 15:11

    еще есть асинхронные генераторы. Пример синтаксиса:


    async function* myFunction() {
      await yield new Promise((resolve) => {});
    }

    вот тут подробнее https://www.youtube.com/watch?v=DqMFX91ToLw


    1. rock
      27.04.2016 15:56

      Ваша информация сильно устарела. На текущий момент, асинхронная итерация и Observable разделены.