JavaScript ES8 и переход на async / await

https://medium.com/@bbrennan/upgrading-to-async-await-with-javascript-es7-3c67602ea7f8
  • Перевод
Недавно мы опубликовали материал «Промисы в 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. Поэтому просим поделиться опытом.
RUVDS.com 553,11
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией
Комментарии 107
  • +2

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


        const user1Promise = hn.getUser({username: 'sama'});
    
        const user2 = await hn.getUser({username: 'pg'});
        const user1 = await user1Promise;
    • +4
      Красивше будет
          const user1Promise = hn.getUser({username: 'sama'});
          const user2Promise = hn.getUser({username: 'pg'});
          
          const user1 = await user1Promise;
          const user2 = await user2Promise;
      

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

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


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

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

        • 0

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


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


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

          • +1

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

            • 0

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


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

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

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

        • 0
          По-моему уж лучше через Promise.all, а то уж больно неочевидно получается, и ошибок так проще наделать.
          • –1
            Есть ещё вариант.
            let user1 = hn.getUser({username: 'sama'});
            let user2 = hn.getUser({username: 'pg'});
            await user1; await user2;
            console.log(user1, user2);
            • 0
              Нет, так работать не будет. То есть конкретно для `console.log` это сработает — но ведь в реальной программе эти запросы не для выдачи в лог делаются…
              • 0
                Что именно работать не будет?
                • 0

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


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

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

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

                      • 0
                        Вы правы.
                        • 0

                          a = await a; b = await b;

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

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

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

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

                  • +2

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

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

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

                      • 0
                        Обилия 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

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

                          • +1
                            Приведите не лукавый код с промисами, пусть даже синтетический (я не могу себе позволить копировать сюда рабочий код, да и вы наверно тоже), в котором оправданы множественные try…catch, а я попробую перевести это в async.
                            Плюс, как я уже писал ниже, никто не запрещает комбинировать async с промисами, если это дает более читабельный код.
                            • –2
                              Вообще-то бремя утверждающего — доказывать утверждение. Если утверждаете, что можно написать красивый код — доказывайте.
                              Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.
                              • +1

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

                                • –1
                                  Вы бы комментарий до конца прочитали (и изучили логику) прежде чем лезть с претензиями. Не пойму, зачем вы лезете, если по сути ничего сказать не можете.
                                  Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.
                                  • +1

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

                                    • –5
                                      А надо было у вас разрешение спросить? Что ж вы о себе возомнили-то?!
                          • +1
                            function withPromises() {
                              return action1().then(r1 => action2().then(r2 => r1 + r2));
                            }
                            • 0
                              Или так, но если в реальности кода будет больше, то придется разворачивать в
                              function withPromises() {
                                return action1().then(r1 => {
                                  return action2().then(r2 => r1 + r2)
                                });
                              }
                              • 0
                                так получиться Promise-hell
                                ИМХО так лучше.
                                function withPromises() {
                                  return action1()
                                  .then(r1 => action2())
                                  .then(r2 => r1 + r2);
                                }
                                

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

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

                                  • 0
                                    согласен, но это не то, что я хотел показать.
                                    В этом примере вообще я не вижу смысла делать 2 промиса последовательно, а если `r1` нужен для `action2()` то его надо туда явно передать.
                      • 0
                        Вы можете комбинировать, 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
                        */
                        
                        • 0
                          При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).

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

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

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


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


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

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

                                Довольно часто так делаю и до сих пор небыло никаких проблем.
                                Да, try-catch иногда выглядят не очень, но в целом стало удобней, чем с промисами.
                                • 0
                                  Завтра попробую откопать, если меня мои восточные друзья ещё из репозитория не выпилили — локально я это точно грохнул.
                                • 0

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

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

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

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

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

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

                                  • –1
                                    потому что если дать лочить основной поток подгрузкой картинок, то так и будут делать. И фризы будут как на криво написанных настольных приложениях
                                    • –1
                                      Справедливости ради, правило «не выполнять что-то долгое в основном потоке» применимо ко многим платформам. А уж в «UI-потоке\коллбеках» — так и вообще ко всем.
                                      • +6
                                        Не пишу на JS, просто зашел посмотреть

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

                                        • +3
                                          Ругать JS за однопоточность это всё равно что ругать С за обязательность указания типов переменных, или java за обязательность классов.
                                          • –2
                                            С тем, кому вы отвечаете я не согласен, но и с вами тоже. Отсутвие многопоточности в javascript это большая проблема и одна из главных причин лагающего UI. Так как нельзя делать что либо не фризя UI.
                                            • 0
                                              Главная причина «лагающего UI» обычно исключительно кривые руки.
                                              Плюс непреодолимое у многих желание нагромождать сущности сверх меры и нагружать код тем, чем он заниматься не должен (например анимацией — скрипт не должен заниматься анимацией, пусть на веб-страничке этим занимается CSS а в реакт-нативе каком-нибудь — нативный код).

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

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


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

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

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

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

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


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

                                                        • 0
                                                          github.com/Microsoft/react-native-windows

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

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


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

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

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

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

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

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

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

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


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

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

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

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


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

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

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

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

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

                                                      На жабоскрипте же первые простенькие скриптики написал в 96 где-то, когда забацал свой homepage на geocities. Правда последующие 17 лет им не пользовался — других забот хватало…
                                                  • +3
                                                    Карму вам не сливал… до этого вашего комментария. Почему когда у людей кончаются аргументы — они начинают вопить о сливе кармы? Как будто карма — аргумент.
                                                    • +1
                                                      П.С.: Спасибо за слив кармы.

                                                      Круто, вы — читер, знаете, кто вам карму сливает??
                                                      • +1
                                                        >В других языках почему-то не обрезали программисту руки, а выполнение чего-то в отдельном потоке просто считается хорошим тоном.

                                                        «Вы либо трусы наденьте, либо крестик снимите» (с)
                                                        В JS тоже никому ничего не обрезали, как и в любых других языках.
                                                        А вот среда выполнения (к которой относятся понятия «UI\core поток») node — да, задает ограничения. Так же как и андроид для джавы, и иОс для свифта. Попробуйте там выполнить что-то долгоиграюющее на UI event ( например при нажатии на кнопку) — тут же наступит «Сначала не дать программисту выполнять что-то долгое в основном потоке, а потом придумывать» (с)

                                                        Так что это не «хороший тон», а необходимость в любой среде и в любом языке, который вы под этой средой исполняете. Чем коллбеки и промисы хуже сишного PostMessage(WM_USER_DO_SOMETHING,...)? А ведь тоже — каноническийъ способ.
                                                    • –1
                                                      Ох уж этот JavaScript… Сначала придумаем Promise, потом надстроим над ним сахар в виде async/await. В итоге за внешней простотой кода скрывается куча сложностей под капотом, что неизбежно приводит к побочным явлениям и неожиданным результатам.
                                                      О чём, собственно, я пытаюсь порассуждать вслух. О том, что история в какой-то момент пошла не туда, и убогий язык стал мэйнстримом, который стали обвешивать костылями. А ведь чем проще решение, тем стабильнее оно работает.
                                                      • +2

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


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

                                                        • 0

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

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

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

                                                          • +1
                                                            Promise придумали 40 лет назад — в JS его только реализовали.
                                                          • –4
                                                            Почему нельзя было, просто добавить блокировку IO в язык?!
                                                            • 0

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

                                                                • 0

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

                                                                  • 0

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

                                                                    • 0

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


                                                                      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()
                                                              • 0

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

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

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


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

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

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

                                                                  • 0
                                                                    Наглядный пример, где использовано и последовательное выполнение и параллельное. Копируйте весь код к себе, вставляйте, запускайте и проверяйте.
                                                                    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}/`);
                                                                    });
                                                                    
                                                                    • 0

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


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

                                                                      • 0
                                                                        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, в любом случае данные либо сразу ушли, либо встали в очередь в памяти на оправку и все равно уйдут, если конечно никаких внезапных крэшей не произойдет)
                                                                        • 0

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

                                                                          • –1
                                                                            Проблема не актуальная, т.к. легко решается масштабированием.
                                                                            Другой вопрос, если денег на масштабирование нету, а нагрузка большая, то тут уже надо не выдумывать костыли на ноде, а на уровне операционной системы ограничивать кол-во соединений с вашим сервером, но тогда будут «лишние» недовольные пользователи, чьи запросы ваш сервер будет отклонять, просто потому, что физически их не сможет обслужить.
                                                                            • 0
                                                                              Если бы проблема была неактуальной и решалась бы масштабированием — то события drain никто бы не вводил.
                                                                              • 0
                                                                                То есть вы хотите сказать, что масштабирование на 100% не решает эту проблему?
                                                                                • +1

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

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

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

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

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

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

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

                                                                      Самое читаемое