Недавно мы опубликовали материал «Промисы в ES6: паттерны и анти-паттерны». Он вызвал серьёзный интерес аудитории, в комментариях к нему наши читатели рассуждали об особенностях написания асинхронного кода в современных JS-проектах. Кстати, советуем почитать их комментарии — найдёте там много интересного.

image

По совету пользователя ilnuribat мы добавили к материалу опрос, целью которого было выяснить популярность промисов, коллбэков и конструкций async / await. По состоянию на 9-е сентября промисы и async / await получили примерно по 43% голосов, с небольшим перевесом async / await, коллбэкам досталось 14%. Главный вывод, который можно сделать, проанализировав результаты опроса и комментарии, заключается в том, что важны все имеющиеся технологии, однако, всё больше программистов тяготеют к async / await. Поэтому сегодня мы решили опубликовать перевод статьи про переход на async / await, которая является продолжением материала о промисах.

Коллбэки, промисы, async / await


На прошлой неделе я писал о промисах, возможности JS, которая появилась в ES6. Промисы были отличным способом вырваться из ада коллбэков. Однако сейчас, когда в Node.js (с версии 7.6.) появилась поддержка async / await, у меня сложилось восприятие промисов как чего-то вроде временного подручного средства. Надо сказать, что async / await можно пользоваться и в браузерном коде благодаря транспиляторам вроде babel.

Хочу сказать, что в этом материале я буду применять самые свежие возможности JS, в том числе — шаблонные литералы и стрелочные функции. Посмотреть список новшеств ES6 можно здесь.

Почему async / await — это замечательно?


До недавних пор асинхронный код в JavaScript, в лучшем случае, выглядел неуклюжим. Для разработчиков, перешедших в JavaScript с таких языков, как Python, Ruby, Java, да практически с любых других, коллбэки и промисы казались неоправданно усложнёнными конструкциями, которые подвержены ошибкам и совершенно сбивают программиста с толку.

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

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

Вот гипотетический пример синхронной версии:

// Примечание: этот код не работает!
let hn = require('@datafire/hacker_news').create();

let storyIDs = hn.getStories({storyType: 'top'});
let topStory = hn.getItem({itemID: storyIDs[0]});
console.log(`Top story: ${topStory.title} - ${topStory.url}`);

Тут всё предельно просто — ничего нового для любого, кто писал на JS. В коде выполняются три шага: получить список идентификаторов материалов, загрузить сведения о самом популярном и вывести результат.

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

Вот — та же логика, реализованная на коллбэках (пример, опять же, гипотетический):

// Примечание: этот код не работает!
let hn = require('@datafire/hacker_news').create();
hn.getStories({storyType: 'top'}, (err, storyIDs) => {
  if (err) throw err;
  hn.getItem({itemID: storyIDs[0]}, (err, topStory) => {
    if (err) throw err;
    console.log(`Top story: ${topStory.title} - ${topStory.url}`);
  })
})

Да уж. Теперь фрагменты кода, реализующие необходимый нам функционал, вложены друг в друга и мы должны их выравнивать по горизонтали. Если бы тут было 20 шагов вместо трёх, то для выравнивания последнего понадобилось бы 40 пробелов! И, если понадобится добавить новый шаг где-нибудь в середине, пришлось бы заново выравнивать всё то, что находится ниже него. Это приводит к появлению огромных и бесполезных различий между разными состояниями файла в Git. Кроме того, обратите внимание на то, что мы должны обрабатывать ошибки на каждом шаге всей этой структуры. Сгруппировать набор операций в одном блоке try / catch не получится.

Попробуем теперь сделать то же самое, воспользовавшись промисами:

let hn = require('@datafire/hacker_news').create();

Promise.resolve()
  .then(_ => hn.getStories({storyType: 'top'}))
  .then(storyIDs => hn.getItem({itemID: storyIDs[0]))
  .then(topStory => console.log(`Top story: ${topStory.title} - ${topStory.url}`))

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

Теперь, разобравшись с обычными функциями, коллбэками и промисами, посмотрим как сделать то же самое с помощью конструкции async / await:

let hn = require('@datafire/hacker_news').create();

(async () => {

  let storyIDs = await hn.getStories({storyType: 'top'});
  let topStory = await hn.getItem({itemID: storyIDs[0]});
  console.log(`Top story: ${topStory.title} - ${topStory.url}`);

})();

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

Тут надо сказать, что методы hn.getStories() и hn.getItem() устроены так, что они возвращают промисы. При их выполнении, цикл событий не блокируется. Благодаря async / await, впервые в истории JS, мы смогли писать асинхронный код, используя обычный декларативный синтаксис!

Переход на async / await


Итак, как же приступить к использованию async / await в своих проектах? Если вы уже работаете с промисами, значит вы готовы к переходу на новую технологию. Любая функция, которая возвращает промис, может быть вызвана с использованием ключевого слова await, что приведёт к тому, что она вернёт результат разрешения промиса. Однако, если вы собираетесь переходить на async / await с коллбэков, вам понадобится сначала преобразовать их в промисы.

?Переход на async / await с промисов


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

Кроме того, блок .catch() надо заменить на стандартные блоки try / catch. Как видите, наконец-то мы можем использовать один и тот же подход для обработки ошибок в синхронном и асинхронном контекстах!

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

let hn = require('@datafire/hacker_news').create();

// Старый код с промисами:
Promise.resolve()
  .then(_ => hn.getStories({storyType: 'top'}))
  .then(storyIDs => hn.getItem({itemID: storyIDs[0]))
  .then(topStory => console.log(topStory))
  .catch(e => console.error(e))

// Новый код с async / await:
(async () => {
  try {
    let storyIDs = await hn.getStories({storyType: 'top'});
    let topStory = await hn.getItem({itemID: storyIDs[0]});
    console.log(topStory);
  }  catch (e) {
    console.error(e);
  }
})();

?Переход на async / await с коллбэков


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

Паттерны и подводные камни


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

?Циклы


Ещё с тех времён, когда я только начинал писать на JS, передача функций в качестве аргументов для других функций была одной из моих любимых возможностей. Конечно, коллбэки — это беспорядок, но я, например, предпочитал использовать Array.forEach вместо обычного цикла for:

const BEATLES = ['john', 'paul', 'george', 'ringo'];

// Обычный цикл for:
for (let i = 0; i < BEATLES.length; ++i) {
  console.log(BEATLES[i]);
}

// Метод Array.forEach:
BEATLES.forEach(beatle => console.log(beatle))

Однако, при использовании await метод Array.forEach правильно работать не будет, так как он рассчитан на выполнение синхронных операций:

let hn = require('@datafire/hacker_news').create();

(async () => {

  let storyIDs = await hn.getStories({storyType: 'top'});
  storyIDs.forEach(async itemID => {
    let details = await hn.getItem({itemID});
    console.log(details);
  });
  console.log('done!'); // Ошибка! Эта команда будет исполнена до того, как все вызовы getItem() будут завершены.

})();

В этом примере forEach запускает кучу одновременных асинхронных обращений к getItem() и немедленно возвращает управление, не ожидая результатов, поэтому первым, что будет выведено на экран, окажется строка «done!».

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

let hn = require('@datafire/hacker_news').create();

(async () => {
  let storyIDs = await hn.getStories({storyType: 'top'});
  
  // Использование цикла for (последовательное выполнение операций)
  for (let i = 0; i < storyIDs.length; ++i) {
    let details = await hn.getItem({itemID: storyIDs[i]});
    console.log(details);
  }
  
  // Использование Promise.all (параллельное выполнение операций)
  let detailSet = await Promise.all(storyIDs.map(itemID => hn.getItem({itemID})));
  detailSet.forEach(console.log);
})();

?Оптимизация


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

Рассмотрим пример. Предположим, мы хотим получить сведения о двух пользователях Hacker News и сравнить их карму. Вот обычная реализация:

let hn = require('@datafire/hacker_news').create();

(async () => {

  let user1 = await hn.getUser({username: 'sama'});
  let user2 = await hn.getUser({username: 'pg'});

  let [more, less] = [user1, user2].sort((a, b) => b.karma - a.karma);
  console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`);

})();

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

let hn = require('@datafire/hacker_news').create();

(async () => {

  let users = await Promise.all([
    hn.getUser({username: 'sama'}),
    hn.getUser({username: 'pg'}),
  ]);

  let [more, less] = users.sort((a, b) => b.karma - a.karma);
  console.log(`${more.id} has more karma (${more.karma}) than ${less.id} (${less.karma})`);

})();

Тут стоит отметить, что прежде чем пользоваться этим методом, стоит удостовериться в том, что желаемого можно достичь за счёт параллельного выполнения команд. Во многих случаях асинхронные операции должны выполняться последовательно.

Итоги


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

Уважаемые читатели! Мы знаем, по результатам опроса из предыдущей публикации, что многие из вас пользуются async / await. Поэтому просим поделиться опытом.

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


  1. mayorovp
    10.10.2017 15:07
    +2

    Оставлю альтернативный вариант распараллеливания для тех случаев когда не получается сделать красиво через Promise.all:


        const user1Promise = hn.getUser({username: 'sama'});
    
        const user2 = await hn.getUser({username: 'pg'});
        const user1 = await user1Promise;


    1. Iqorek
      10.10.2017 21:44
      +4

      Красивше будет

          const user1Promise = hn.getUser({username: 'sama'});
          const user2Promise = hn.getUser({username: 'pg'});
          
          const user1 = await user1Promise;
          const user2 = await user2Promise;
      

      Так имхо читабельней.


      1. justboris
        10.10.2017 22:32
        +3

        Тогда уж и до такого недалеко


        const [user1, user2] = await Promise.all([
          hn.getUser({username: 'sama'}),
          hn.getUser({username: 'pg'})
        ])

        Кстати, mayorovp, а чем вам этот вариант не подошел?


        1. mayorovp
          10.10.2017 22:38

          Конкретно в данном случае он подходит. Но так бывает не всегда.


          Сталкивался пару раз с ситуацией, когда надо в старом коде запустить параллельный процесс. Переводить на Promise.all в таком случае означает изменить 30 строк, что выльется в приключения с git rebase перед пушем если кто-то еще правил этот метод и затруднит git blame в следующие пять лет поскольку сделает меня автором строк которые я не писал.


          А альтернативный подход — это всего 2 измененные строки.


          1. dfuse
            11.10.2017 02:35
            +1

            А я вот считаю, что лучше тронуть 30 строк и написать новое красивое решение (которым можно гордиться ближайшие 5 лет), чем костылить 2 и получать бяку в итоге :)


            1. mayorovp
              11.10.2017 06:07

              А я вот не считаю использование Promise.all настолько сложным чтобы его использованием можно было гордиться 5 лет...


              Для рефакторинга же придет время когда я останусь один на проекте :-)


      1. ingumsky
        11.10.2017 00:39

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


        1. Chamie
          12.10.2017 16:56
          -1

          Так не заводите констант. Просто по месту использования приписывайте await перед переменной с промисом.


    1. YemSalat
      11.10.2017 13:39

      По-моему уж лучше через Promise.all, а то уж больно неочевидно получается, и ошибок так проще наделать.


    1. Svan
      11.10.2017 13:52
      -1

      Есть ещё вариант.

      let user1 = hn.getUser({username: 'sama'});
      let user2 = hn.getUser({username: 'pg'});
      await user1; await user2;
      console.log(user1, user2);


      1. mayorovp
        11.10.2017 14:06

        Нет, так работать не будет. То есть конкретно для `console.log` это сработает — но ведь в реальной программе эти запросы не для выдачи в лог делаются…


        1. Svan
          11.10.2017 14:13

          Что именно работать не будет?


          1. mayorovp
            11.10.2017 14:20

            То, что вы написали, работать не будет.


            Оно выведет что-то типа Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }} Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }} вместо данных двух пользователей.


            1. Svan
              11.10.2017 14:21

              Странно, мне выводит данные двух пользователей.


              1. mayorovp
                11.10.2017 14:26

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


                1. Svan
                  11.10.2017 14:26

                  Вы правы.


                1. Svan
                  11.10.2017 14:32


                  a = await a; b = await b;

                  можно типа такого. Хотя так уже мне самому не особо нравится.


  1. Aingis
    10.10.2017 16:02
    +1

    Это всё здорово, но за бортом остался один крайне важный вопрос: обработка ошибок. А если вы начнёте заворачивать код в `try…catch`, красота по сравнению с промисами нивелируется. При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).


    1. mayorovp
      10.10.2017 16:36

      Почему try…catch нивелирует красоту?


      1. deniss-s
        10.10.2017 18:33

        try..catch увеличивает уровень вложенность


        1. ankh1989
          10.10.2017 18:51

          Вы же не пишете по два колбека на каждый .then? Так и здесь — try..catch только в конце — на самом верхнем уровне.


      1. Aingis
        10.10.2017 19:00
        +2

        Можете привести красивый код с обилием try…catch и async/await, который был бы явно лучше промисов? В статье, что характерно, лишь один неприметный и не очень выразительный пример обработки ошибки.


        1. parakhod
          10.10.2017 19:21
          +3

          Вот абсолютно соглашусь. Причём, лично на мой вкус ещё и читаемость снижается: всё-таки JavaScript асинхронный язык — ну и оставьте его таким.
          А промизы, к тому же, и комбинировать значительно удобнее. Особенно если задача становится чуть сложнее тривиальной.


          1. mayorovp
            10.10.2017 20:37

            Зато код в стиле async/await удобнее отлаживать.


        1. Iqorek
          10.10.2017 23:32

          Обилия try…catch быть не должно в любом коде, это признак, что то не так, async лучше если нужно использовать результаты нескольких асинхронных операций вместе, например:

          async function action1() {
            return 1;
          }
          
          async function action2() {
            return 2;
          }
          
          // promises
          function withPromises() {
            let r1;
            return action1()
              .then(r => {
                r1 = r;
              })
              .then(action2)
              .then(r => r1 + r)
          }
          
          withPromises()
            .then(r => console.log('promise', r))
            .catch(e => console.error(e));
          
          // async
          async function withAsync() {
            return await action1() + await action2();
          }
          
          (async() => {
            try {
              console.log('async', await withAsync());
            } catch (e) {
              console.error(e)
            }
          })()
          

          async намного короче и лучше читабельней.


          1. Aingis
            11.10.2017 12:01
            -1

            Опять лукавый код (и довольно синтетический), только один try…catch. В реальном коде надо обрабатывать разные ошибки в разных местах, да и действия посложнее простого сложения.


            1. Iqorek
              11.10.2017 12:26
              +1

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


              1. Aingis
                11.10.2017 13:03
                -2

                Вообще-то бремя утверждающего — доказывать утверждение. Если утверждаете, что можно написать красивый код — доказывайте.
                Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.


                1. mayorovp
                  11.10.2017 13:08
                  +1

                  Вообще-то вы первый начали утверждать что try...catch нивелирует красоту, так что не переводите стрелки.


                  1. Aingis
                    11.10.2017 14:05
                    -1

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

                    Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.


                    1. mayorovp
                      11.10.2017 14:07
                      +1

                      Ну и зачем вы в таком случае написали комментарий, который в принципе недоказуем?


                      1. Aingis
                        11.10.2017 15:06
                        -5

                        А надо было у вас разрешение спросить? Что ж вы о себе возомнили-то?!


          1. Stepanya
            11.10.2017 14:15
            +1

            function withPromises() {
              return action1().then(r1 => action2().then(r2 => r1 + r2));
            }


            1. Iqorek
              11.10.2017 16:23

              Или так, но если в реальности кода будет больше, то придется разворачивать в

              function withPromises() {
                return action1().then(r1 => {
                  return action2().then(r2 => r1 + r2)
                });
              }


              1. RidgeA
                11.10.2017 16:43

                так получиться Promise-hell
                ИМХО так лучше.

                function withPromises() {
                  return action1()
                  .then(r1 => action2())
                  .then(r2 => r1 + r2);
                }
                

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


                1. justboris
                  11.10.2017 17:42

                  у вас r1 в замыкании теряется. В последней строчке будет r1 is not defined.


                  1. RidgeA
                    11.10.2017 17:47

                    согласен, но это не то, что я хотел показать.
                    В этом примере вообще я не вижу смысла делать 2 промиса последовательно, а если `r1` нужен для `action2()` то его надо туда явно передать.


    1. Iqorek
      10.10.2017 22:16

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

      async function getData() {
        let result = await asyncAction(10);
      
        console.log('result 1:', result);
        result += await asyncAction(20, true);
      
        console.log('unreachable code');
        result += await asyncAction(30);
        return result;
      }
      
      async function asyncAction(value, throwException = false) {
        if (throwException) {
          throw new Error(':(');
        }
        return value*2;
      }
      
      (async() => {
        console.log('begin getData');
      
        const data = await getData().catch(e => { // <===
          console.log('error occurred', e.message);
          return -1;
        })
      
        console.log('end getData', data);
      })()
      
      
      /* 
      output: 
         begin getData
         result 1: 20
         error occurred :(
         end getData -1
      */
      


    1. mayorovp
      11.10.2017 13:10

      При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).

      Вообще-то поддерживается с 7й версии если про ключ --harmony не забывать.


  1. parakhod
    10.10.2017 16:25
    -2

    К сожалению, этот кажущийся поначалу действительно красивым и изящным подход, несёт в себе столько потенциальных источников ошибок, что я пока воздерживаюсь от его использования и не уверен, что буду использовать когда-либо вообще.
    Недавно меня попросили отловить ошибку в одном приложении на ReactNative — почему-то иногда всё очень сильно тормозило, а иногда вообще зависало, причём без каких-либо сообщений об ошибках в консоли. Оказалось, что кто-то из разработчиков в одном очень второстепенном компоненте решил объявить метод componentWillMount() как async, а внутрь напихать асинхронных функций.
    И это работало. Иногда.
    Когда же что-то там переклинивало, то компонент просто переставал монтироваться, и всё зависало в его ожидании.


    1. mayorovp
      10.10.2017 16:45

      Не вижу способа как асинхронный componentWillMount может хоть что-нибудь сломать. Вы можете привести какие-нибудь подробности?


      Я поискал на гитхабе места где React вызывает componentWillMount — но я не нашел с ходу ни одного места где возвращаемое из componentWillMount хоть как-то использовалось бы.


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


      1. parakhod
        10.10.2017 17:14

        Ни componentWillMount ни componentDidMount ничего не возвращают и вроде как не должны (в отличие от shouldComponentUpdate например).
        Код был индийский и достаточно страшный — там были не просто синхронные функции — там было страшное месиво из await'ов, промизов, вызовов экшнов с промизами и так далее.
        Но факт остаётся фактом — как только я убрал с componentWillMount async, а единственный await заменил коллбеком, всё заработало. Возникавшая же иногда ошибка стала отлавливаться в .catch промиза из экшнов, чего до тех пор не происходило.
        Глубже копаться времени не было.


        1. Akuma
          10.10.2017 20:20
          +1

          А можете скинуть реализацию метода? Прям интересно стало. Подозреваю, что оно просто грузило процессор какой-то неведомой фигней и async тут не при чем.

          Довольно часто так делаю и до сих пор небыло никаких проблем.
          Да, try-catch иногда выглядят не очень, но в целом стало удобней, чем с промисами.


          1. parakhod
            10.10.2017 20:48

            Завтра попробую откопать, если меня мои восточные друзья ещё из репозитория не выпилили — локально я это точно грохнул.


        1. kwolfy
          11.10.2017 11:21

          Обернули бы этот await в try/catch и ошибка так же отлавливалась бы


          1. parakhod
            11.10.2017 11:34
            -1

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


            1. kwolfy
              12.10.2017 09:38

              Конкретно в вашем случае может и нет смысла. Я пытался донести, что проблема была вовсе не в async await, а в неправильной обработке исключений.


  1. vanxant
    10.10.2017 16:43
    +7

    обычный декларативный синтаксис

    всё же, императивный.


  1. Revertis
    10.10.2017 17:11
    -12

    Не пишу на JS, просто зашел посмотреть. Какой же это ужас всё-таки. Сначала не дать программисту выполнять что-то долгое в основном потоке, а потом придумывать (уже третью итерацию) кучу костылей.


    1. mayorovp
      10.10.2017 17:19
      +5

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


    1. Aquahawk
      10.10.2017 17:36
      -1

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


    1. vassabi
      10.10.2017 17:38
      -1

      Справедливости ради, правило «не выполнять что-то долгое в основном потоке» применимо ко многим платформам. А уж в «UI-потоке\коллбеках» — так и вообще ко всем.


    1. justboris
      10.10.2017 18:28
      +6

      Не пишу на JS, просто зашел посмотреть

      "Ничего в этом не понимаю, но мнение имею".


    1. parakhod
      10.10.2017 19:28
      +3

      Ругать JS за однопоточность это всё равно что ругать С за обязательность указания типов переменных, или java за обязательность классов.


      1. Lailore
        11.10.2017 13:54
        -2

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


        1. parakhod
          11.10.2017 14:43

          Главная причина «лагающего UI» обычно исключительно кривые руки.
          Плюс непреодолимое у многих желание нагромождать сущности сверх меры и нагружать код тем, чем он заниматься не должен (например анимацией — скрипт не должен заниматься анимацией, пусть на веб-страничке этим занимается CSS а в реакт-нативе каком-нибудь — нативный код).

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


          1. vintage
            11.10.2017 20:31
            -3

            CSS анимации происходят в том же потоке, что и исполнение скриптов. Из-за этого исполнение скриптов приходится дробить на кванты по 15мс.


            Многопоточность лучше использовать без разделяемого состояния.


            1. parakhod
              12.10.2017 10:44
              +2

              Вы уверены что так происходит во всех движках?
              Вы уверены что в следующей версии того движка, в котором, как вы считаете, так происходит, всё останется так же?
              Вы уверены что динамически компилируемый код скрипта будет крутить анимацию так же быстро как нативный код?
              И да, UI на js это не только веб-страницы.


              1. vintage
                12.10.2017 12:42

                Насчёт CSS я вас обманул. Был введён в заблуждение этой демкой, где оказывается анимация сделана через js, а не css: https://build-mbfootjxoo.now.sh/


                1. parakhod
                  12.10.2017 12:54

                  Да не вопрос, не за что извиняться.
                  Просто это универсальный принцип — не требуем от скрипта больше, чем может дать нам скрипт.
                  Я в своё время поругался из-за этого с некоторыми адептами перевода анимации в JS на ReactNative (RN — это классная технология, но, к сожалению, среди её разработчиков слишком много очень самоуверенных товарищей, явно ещё не находившихся по классическим граблям). В результате я просто давно забил на споры и потратил пару дней на написание нативного AnimatedView с точно теми же пропсами что и канонический <Animated.View>, но только нативной анимацией — я просто беру и подменяю им все анимации в библиотеках которые вставляю в свои проекты. А когда у меня спрашивают «а как ты сделал так что у тебя тут не тормозит, всегда же тормозит!» я просто загадочно улыбаюсь.


                  1. vintage
                    12.10.2017 13:02

                    Там уже побороли неработающий JIT под iOS? Добавили поддержку Win? Понаписали кроссплатформенных компонент, не требующих писать разный код для разных платформ?


                    По мне так лучше кордова с css анимациями в возможностью запуска в вебе или xamarin с полноценным компилируемым языком.


                    1. parakhod
                      12.10.2017 13:16

                      github.com/Microsoft/react-native-windows

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


                      1. vintage
                        12.10.2017 13:31

                        По винде допустим ОК, хотя поддерживается сторонней компанией.


                        А почему бы их не посравнивать? И это вы RN сейчас полноценной платформой назвали, который не более чем контейнер для JS?


                        1. parakhod
                          12.10.2017 13:43

                          Ну дык а Android — унылый контейнер для Java (которая по факту бывает медленнее работает чем тот JS), где всё что должно шевелиться быстрее черепахи приходится писать под NDK, что уж мелочиться…

                          Разница в реализации UI, да. В RN это нативные View у мобильных платформ и окна в винде, в вебаппах это webview. Попробуйте на досуге продать это клиенту за те же деньги…


                          1. vintage
                            12.10.2017 14:26
                            -1

                            Я продам в 2 раза дешевле и сделаю в 2 раза быстрее сразу под 4 платформы.


                            1. parakhod
                              12.10.2017 15:14

                              Ну вперёд, кто ж держит-то )))
                              Вебаппы, правда, по-моему уже даже континентальные китайцы брезгуют заказывать, но отчего б не попробовать…


                              1. vintage
                                12.10.2017 15:55
                                +1

                                Что не так с веб-аппами?


          1. sshikov
            11.10.2017 20:50
            -1

            Главная причина «лагающего UI» обычно исключительно кривые руки.

            Ну да, разумеется — чьи-то кривые руки это причина большинства проблем, и не только в UI. Но тем не менее, для UI вообще характерна однопоточность (не вообще приложения, а только один поток работает с UI), и если вы посмотрите — то множество широко известных фреймворков сделаны именно так. И на то есть серьезные причины.


            А кривые руки — уже последствия той сложности, которая при этом возникает.


    1. Revertis
      10.10.2017 20:05
      -8

      Почему это я не могу иметь мнение о языке, даже если я им не пользуюсь, но пользуюсь другими более 15-ти лет?
      В других языках почему-то не обрезали программисту руки, а выполнение чего-то в отдельном потоке просто считается хорошим тоном.
      Я работаю с UI уже не одно десятилетие, и знаю что где и как тормозит, зачем сразу унижать оппонента, если его не знаете?

      П.С.: Спасибо за слив кармы.
      mayorovp, Aquahawk, vassabi, justboris, parakhod.


      1. justboris
        10.10.2017 20:18

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


        Практической пользы от таких набросов — ноль.


      1. parakhod
        10.10.2017 20:22

        Конечно вы можете иметь своё мнение, просто когда у вас наступит 30 лет опыта пользования, как у меня, например, ваше мнение станет гораздо спокойнее.

        Кстати, я вам карму не сливал, она у меня у самого отрицательная ))
        В наши времена даже спокойное мнение, не совпадающее с мнением большинства, склонно вызывать раздражение…


        1. sshikov
          10.10.2017 22:28

          Про 30 лет — это вы пожалуй загнули :) javascript как языку всего примерно 21 год (в 1995 кажется он появился).


          1. parakhod
            10.10.2017 22:46

            30 лет «пользования другими языками».
            В сентябре 1987 года впервые сел за чудовище под названием Агат-7, и начал пытаться писать что-то на его жутковатом бейсике.
            Так что ровно 30 ))

            На жабоскрипте же первые простенькие скриптики написал в 96 где-то, когда забацал свой homepage на geocities. Правда последующие 17 лет им не пользовался — других забот хватало…


      1. mayorovp
        10.10.2017 20:27
        +3

        Карму вам не сливал… до этого вашего комментария. Почему когда у людей кончаются аргументы — они начинают вопить о сливе кармы? Как будто карма — аргумент.


      1. TheShock
        10.10.2017 20:50
        +1

        П.С.: Спасибо за слив кармы.

        Круто, вы — читер, знаете, кто вам карму сливает??


  1. PaulZi
    10.10.2017 19:07
    -1

    Ох уж этот JavaScript… Сначала придумаем Promise, потом надстроим над ним сахар в виде async/await. В итоге за внешней простотой кода скрывается куча сложностей под капотом, что неизбежно приводит к побочным явлениям и неожиданным результатам.
    О чём, собственно, я пытаюсь порассуждать вслух. О том, что история в какой-то момент пошла не туда, и убогий язык стал мэйнстримом, который стали обвешивать костылями. А ведь чем проще решение, тем стабильнее оно работает.


    1. mayorovp
      10.10.2017 20:36
      +2

      Монада Promise — это общий подход, который сейчас применяется в языках C#, Python, Java (тут пока без синтаксиса async/await), хотят ввести в С++. Видел библиотеку и для Ruby, но не уверен насчет популярности.


      В любом случае, из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом "программист должен страдать" и Haskell с его ленивыми вычислениями. Вы точно уверены что обещания — это костыли, а не новая парадигма асинхронного программирования?


      1. sshikov
        10.10.2017 22:17

        В смысле — старая парадигма? Насколько я помню, этой парадигме лет 10 уже наверное минуло (не в виде async/await, а в изначальном).


      1. lega
        11.10.2017 09:34

        из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом «программист должен страдать»
        Поподробнее пожалуйста.


        1. mayorovp
          11.10.2017 12:25

          Это была ирония. На самом деле это не девиз языка, а мое восприятие криков фанатиков go про дизайн их языка.


    1. Enverest
      11.10.2017 14:15
      +1

      Promise придумали 40 лет назад — в JS его только реализовали.


  1. dimka11
    10.10.2017 22:01
    -4

    Почему нельзя было, просто добавить блокировку IO в язык?!


    1. sshikov
      10.10.2017 22:25

      Можете пояснить, как это можно было сделать "просто"? Ну, так чтобы не сломать все, что было до этого?


      1. vintage
        11.10.2017 04:12
        -1

        1. sshikov
          11.10.2017 20:36

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


          1. vintage
            11.10.2017 21:52

            Да, с ним всё куда проще, чем с async/await.
            В браузере никак ибо не стандарт.
            Да нет, там всё в одном потоке исполняется, никакой особой синхронизации не нужно.


            1. vintage
              11.10.2017 21:59

              Ну, например, неработающий код из статьи:


              let hn = require('@datafire/hacker_news').create();
              
              (async () => {
              
                let storyIDs = await hn.getStories({storyType: 'top'});
                storyIDs.forEach(async itemID => {
                  let details = await hn.getItem({itemID});
                  console.log(details);
                });
                console.log('done!'); // Ошибка! Эта команда будет исполнена до того, как все вызовы getItem() будут завершены.
              
              })();

              C node-fibers можно переписать так:


              const hn = require('@datafire/hacker_news').create();
              const Future = retuire( 'fibers/future' )
              
              Future.task(() => {
              
                let storyIDs = hn.getStories({storyType: 'top'}).wait();
                storyIDs.forEach( itemID => {
                  let details = hn.getItem({itemID}).wait();
                  console.log(details);
                });
                console.log('done!');
              
              }).detach()


    1. justboris
      10.10.2017 23:28

      IO является не частью языка, а платформы. Для Javascript такой платформой является Node.js.
      И блокирующие IO-операции там есть.


  1. musuk
    11.10.2017 04:05

    Попробовал я один AngularJS-проект с промисов на async/await переписать, еще всякие let, const, и стрелочные функции использовать. Babel настроил, как мог.
    Получилось, конечно, красиво. Только вот у AngularJS свои собственные промисы, которые умеют делать $apply(). Можно, конечно windows.Promise переопределить, но у меня на проекте есть куча разных сторонних библиотек не связанных с ангуляром. Так что пришлось вызывать $scope.$apply() явно.
    Стали очень плохо работать брякпоинты в Chrome Developer Console. Причем заметил я это, когда большая часть проекта уже была переписана. Мучал я babel, читал форумы, но как я понял, с отладкой es7 кода на браузере как-то все не очень радужно.


    1. mayorovp
      11.10.2017 06:12

      Хром давно умеет async/await нативно, в дев-сборке надо лишние плагины в babel по-отключать было.


      А проблема своих промисов Ангуляра решается использованием генераторов обернутых в интерпретатор вместо асинхронных функций. Вот решение аналогичной проблемы в mobx-utils: https://github.com/mobxjs/mobx-utils/blob/master/src/async-action.ts


  1. MishUshakov
    11.10.2017 14:15

    Интересная тема, но насколько она практичная?


    1. wert_lex
      11.10.2017 19:50

      Более чем практичная. Проект на 75kloc — сначала перевели тесты через бабель еще во времена 6-й ноды (~33kloc). Потом перевели всё остальное уже с 8-й нодой. Все довольны очень сильно, всё здорово.


  1. MaZaAa
    14.10.2017 14:16

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

    const http = require('http');
    
    let promise = (payload, timeout = 10) => new Promise((resolve, reject) => {
        setTimeout(() => {
            resolve(payload);
        }, timeout);
    });
    
    
    const server = http.createServer((req, res) => {
        res.statusCode = 200;
        res.setHeader('Content-Type', 'text/html; charset=UTF-8');
        (async () => {
            try {
                // Шаг 1: Ждем пока параллельно выполнится Promise.all (таймауты в 20ms)
                let results = await Promise.all([
                    promise("[1]", 20),
                    promise("[2]", 20)
                ]);
                // После того как дождались колбеков из Promise.all, запишем в сокет результат
                results.forEach(r => res.write(r));
    
                // Шаг 2: Ждем пока параллельно выполнится Promise.all (таймауты в 5ms)
                results = await Promise.all([
                    promise("[3]", 5),
                    promise("[4]", 5)
                ]);
                // После того как дождались колбеков из Promise.all, запишем в сокет результат
                results.forEach(r => res.write(r.toString()));
    
                // Шаг 3: Ждем пока поочередно выполнятся 2 промиса
                const a = await promise(1, 10);
                const b = await promise(3, 10);
                // Запишем промежуточный результат ввиде суммы переданных оргуметов
                res.write(`---Sum: ${(a + b)}---`);
    
                // Шаг 4: Ждем пока параллельно выполнится Promise.all (таймауты в 5ms)
                results = await Promise.all([
                    promise("[5]", 5),
                    promise("[6]", 5)
                ]);
                // После того как дождались колбеков из Promise.all, запишем в сокет результат
                results.forEach(r => res.write(r.toString()));
    
                // Ждем последовательного выполнения в цикле
                for (let i = 7; i <= 10; i++) {
                    res.write(await promise(`[${i}]`, 100));
                }
    
                res.end(); // Закрываем соединение и сокет
            } catch (e) {
                console.log(e);
                res.end('Error');
            }
        })();
    });
    
    const port = 8081;
    server.listen(port, '0.0.0.0', 65535, () => {
        console.log(`Server running at http://localhost:${port}/`);
    });
    


    1. mayorovp
      14.10.2017 14:31

      Мне кажется, тут один уровень вложенности лишний: можно либо async выше перенести (http.createServer(async (req, res) => { ... }) либо try-catch ((async () => { ... })().catch(e => { ... }))


      И я бы еще проверял результат вызова write и ждал события drain если вернулось false.


      1. MaZaAa
        14.10.2017 14:49

        res.write(...):
        Returns true if the entire data was flushed successfully to the kernel buffer. Returns false if all or part of the data was queued in user memory. 'drain' will be emitted when the buffer is free again.

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


        1. mayorovp
          14.10.2017 16:31

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


          1. MaZaAa
            14.10.2017 17:03
            -1

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


            1. mayorovp
              14.10.2017 17:10

              Если бы проблема была неактуальной и решалась бы масштабированием — то события drain никто бы не вводил.


              1. MaZaAa
                14.10.2017 17:15

                То есть вы хотите сказать, что масштабирование на 100% не решает эту проблему?


                1. mayorovp
                  14.10.2017 17:16
                  +1

                  Конечно же не решает. Хакеру пофиг сколько серверов ложить атакой медленного чтения.


                  1. MaZaAa
                    14.10.2017 17:29

                    Так ваше решение не спасает от этого =) Так или иначе пользователи не будут получать ответ от сервера.
                    От такой атаки нужно защищаться по другому. Например установить таймаут на все соединения, например если в течении 3-5 секунд соединение все ещё висит, то обрубаем его. И если с этого айпишника приходит более N таких соединений за N время, то вообще блокируем его на N часов.


                    1. mayorovp
                      14.10.2017 17:36

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


                      1. MaZaAa
                        14.10.2017 18:00

                        Я надеюсь вы понимаете, что в случае настоящей DDoS атаки, а не баловства ляжет 99.9% всех проектов. Поэтому не нужно параноить и создавать себе иллюзии, что ваш VDS с 1 гигом оперативы и один ядром процессора, становится неуязвимым против атак, если вы проверяете событие drain)


                        1. mayorovp
                          14.10.2017 18:01

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


                          1. MaZaAa
                            14.10.2017 18:06

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


  1. MaZaAa
    14.10.2017 17:54

    deleted