
По совету пользователя 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)
Aingis
10.10.2017 16:02+1Это всё здорово, но за бортом остался один крайне важный вопрос: обработка ошибок. А если вы начнёте заворачивать код в `try…catch`, красота по сравнению с промисами нивелируется. При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).
mayorovp
10.10.2017 16:36Почему
try…catch
нивелирует красоту?Aingis
10.10.2017 19:00+2Можете привести красивый код с обилием
try…catch
иasync
/await
, который был бы явно лучше промисов? В статье, что характерно, лишь один неприметный и не очень выразительный пример обработки ошибки.parakhod
10.10.2017 19:21+3Вот абсолютно соглашусь. Причём, лично на мой вкус ещё и читаемость снижается: всё-таки JavaScript асинхронный язык — ну и оставьте его таким.
А промизы, к тому же, и комбинировать значительно удобнее. Особенно если задача становится чуть сложнее тривиальной.
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 намного короче и лучше читабельней.Aingis
11.10.2017 12:01-1Опять лукавый код (и довольно синтетический), только один
try…catch
. В реальном коде надо обрабатывать разные ошибки в разных местах, да и действия посложнее простого сложения.Iqorek
11.10.2017 12:26+1Приведите не лукавый код с промисами, пусть даже синтетический (я не могу себе позволить копировать сюда рабочий код, да и вы наверно тоже), в котором оправданы множественные try…catch, а я попробую перевести это в async.
Плюс, как я уже писал ниже, никто не запрещает комбинировать async с промисами, если это дает более читабельный код.Aingis
11.10.2017 13:03-2Вообще-то бремя утверждающего — доказывать утверждение. Если утверждаете, что можно написать красивый код — доказывайте.
Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.mayorovp
11.10.2017 13:08+1Вообще-то вы первый начали утверждать что try...catch нивелирует красоту, так что не переводите стрелки.
Aingis
11.10.2017 14:05-1Вы бы комментарий до конца прочитали (и изучили логику) прежде чем лезть с претензиями. Не пойму, зачем вы лезете, если по сути ничего сказать не можете.
Утверждение «нельзя написать красивый код» в принципе не доказуемо, зато достаточно привести один опровергающий пример.
Stepanya
11.10.2017 14:15+1function withPromises() { return action1().then(r1 => action2().then(r2 => r1 + r2)); }
Iqorek
11.10.2017 16:23Или так, но если в реальности кода будет больше, то придется разворачивать в
function withPromises() { return action1().then(r1 => { return action2().then(r2 => r1 + r2) }); }
RidgeA
11.10.2017 16:43так получиться Promise-hell
ИМХО так лучше.
function withPromises() { return action1() .then(r1 => action2()) .then(r2 => r1 + r2); }
Понятно, что если на каждом этапе надо делать больше, чем просто вызвать следующую функцию или сложить 2 значения, то надо будет разворачивать стрелочные функции в нормальные или выносить этот функционал в отдельные именованные функции.justboris
11.10.2017 17:42у вас r1 в замыкании теряется. В последней строчке будет
r1 is not defined
.RidgeA
11.10.2017 17:47согласен, но это не то, что я хотел показать.
В этом примере вообще я не вижу смысла делать 2 промиса последовательно, а если `r1` нужен для `action2()` то его надо туда явно передать.
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 */
mayorovp
11.10.2017 13:10При этом промисы поддерживаются с 4 версии ноды (практически везде), а async/await только с 8 (которая ещё не вышла в LTS).
Вообще-то поддерживается с 7й версии если про ключ
--harmony
не забывать.
parakhod
10.10.2017 16:25-2К сожалению, этот кажущийся поначалу действительно красивым и изящным подход, несёт в себе столько потенциальных источников ошибок, что я пока воздерживаюсь от его использования и не уверен, что буду использовать когда-либо вообще.
Недавно меня попросили отловить ошибку в одном приложении на ReactNative — почему-то иногда всё очень сильно тормозило, а иногда вообще зависало, причём без каких-либо сообщений об ошибках в консоли. Оказалось, что кто-то из разработчиков в одном очень второстепенном компоненте решил объявить метод componentWillMount() как async, а внутрь напихать асинхронных функций.
И это работало. Иногда.
Когда же что-то там переклинивало, то компонент просто переставал монтироваться, и всё зависало в его ожидании.mayorovp
10.10.2017 16:45Не вижу способа как асинхронный
componentWillMount
может хоть что-нибудь сломать. Вы можете привести какие-нибудь подробности?
Я поискал на гитхабе места где React вызывает
componentWillMount
— но я не нашел с ходу ни одного места где возвращаемое изcomponentWillMount
хоть как-то использовалось бы.
Рискну предположить что либо среди "напиханных" асинхронных функций попалась одна синхронная, из-за которой все и висло, либо ожидание было реализовано уже силами того программиста. В любом случае, async/await тут ни при чем — на промизах все точно так же переклинило бы.
parakhod
10.10.2017 17:14Ни componentWillMount ни componentDidMount ничего не возвращают и вроде как не должны (в отличие от shouldComponentUpdate например).
Код был индийский и достаточно страшный — там были не просто синхронные функции — там было страшное месиво из await'ов, промизов, вызовов экшнов с промизами и так далее.
Но факт остаётся фактом — как только я убрал с componentWillMount async, а единственный await заменил коллбеком, всё заработало. Возникавшая же иногда ошибка стала отлавливаться в .catch промиза из экшнов, чего до тех пор не происходило.
Глубже копаться времени не было.Akuma
10.10.2017 20:20+1А можете скинуть реализацию метода? Прям интересно стало. Подозреваю, что оно просто грузило процессор какой-то неведомой фигней и async тут не при чем.
Довольно часто так делаю и до сих пор небыло никаких проблем.
Да, try-catch иногда выглядят не очень, но в целом стало удобней, чем с промисами.parakhod
10.10.2017 20:48Завтра попробую откопать, если меня мои восточные друзья ещё из репозитория не выпилили — локально я это точно грохнул.
kwolfy
11.10.2017 11:21Обернули бы этот await в try/catch и ошибка так же отлавливалась бы
parakhod
11.10.2017 11:34-1Ну а какой тогда смысл вообще в использовании этого синтаксиса? Кода и фигурных скобок уже и так оказывается больше, чем без него…
kwolfy
12.10.2017 09:38Конкретно в вашем случае может и нет смысла. Я пытался донести, что проблема была вовсе не в async await, а в неправильной обработке исключений.
Revertis
10.10.2017 17:11-12Не пишу на JS, просто зашел посмотреть. Какой же это ужас всё-таки. Сначала не дать программисту выполнять что-то долгое в основном потоке, а потом придумывать (уже третью итерацию) кучу костылей.
mayorovp
10.10.2017 17:19+5"не дать программисту выполнять что-то долгое в основном потоке" — это общее свойство всех правильных подходов к построению UI, потому что альтернатива — фризы и подгаливания.
Aquahawk
10.10.2017 17:36-1потому что если дать лочить основной поток подгрузкой картинок, то так и будут делать. И фризы будут как на криво написанных настольных приложениях
vassabi
10.10.2017 17:38-1Справедливости ради, правило «не выполнять что-то долгое в основном потоке» применимо ко многим платформам. А уж в «UI-потоке\коллбеках» — так и вообще ко всем.
justboris
10.10.2017 18:28+6Не пишу на JS, просто зашел посмотреть
"Ничего в этом не понимаю, но мнение имею".
parakhod
10.10.2017 19:28+3Ругать JS за однопоточность это всё равно что ругать С за обязательность указания типов переменных, или java за обязательность классов.
Lailore
11.10.2017 13:54-2С тем, кому вы отвечаете я не согласен, но и с вами тоже. Отсутвие многопоточности в javascript это большая проблема и одна из главных причин лагающего UI. Так как нельзя делать что либо не фризя UI.
parakhod
11.10.2017 14:43Главная причина «лагающего UI» обычно исключительно кривые руки.
Плюс непреодолимое у многих желание нагромождать сущности сверх меры и нагружать код тем, чем он заниматься не должен (например анимацией — скрипт не должен заниматься анимацией, пусть на веб-страничке этим занимается CSS а в реакт-нативе каком-нибудь — нативный код).
Если же в джаваскрипте появится реальная многопоточность, да со всякими там мьютексами и прочей привычной нам и нежно любимой нечистью, при том что в отличие от других языков JS строгостью и ограничениями, мягко говоря, не отличается, да и многие программисты на JS, к сожалению, обладают мнением о собственной квалификации чуть большим, чем следовало бы — количество ошибок которые из-за этого возникнет моментально перечеркнёт все возможные потенциальные плюсы.vintage
11.10.2017 20:31-3CSS анимации происходят в том же потоке, что и исполнение скриптов. Из-за этого исполнение скриптов приходится дробить на кванты по 15мс.
Многопоточность лучше использовать без разделяемого состояния.
parakhod
12.10.2017 10:44+2Вы уверены что так происходит во всех движках?
Вы уверены что в следующей версии того движка, в котором, как вы считаете, так происходит, всё останется так же?
Вы уверены что динамически компилируемый код скрипта будет крутить анимацию так же быстро как нативный код?
И да, UI на js это не только веб-страницы.vintage
12.10.2017 12:42Насчёт CSS я вас обманул. Был введён в заблуждение этой демкой, где оказывается анимация сделана через js, а не css: https://build-mbfootjxoo.now.sh/
parakhod
12.10.2017 12:54Да не вопрос, не за что извиняться.
Просто это универсальный принцип — не требуем от скрипта больше, чем может дать нам скрипт.
Я в своё время поругался из-за этого с некоторыми адептами перевода анимации в JS на ReactNative (RN — это классная технология, но, к сожалению, среди её разработчиков слишком много очень самоуверенных товарищей, явно ещё не находившихся по классическим граблям). В результате я просто давно забил на споры и потратил пару дней на написание нативного AnimatedView с точно теми же пропсами что и канонический <Animated.View>, но только нативной анимацией — я просто беру и подменяю им все анимации в библиотеках которые вставляю в свои проекты. А когда у меня спрашивают «а как ты сделал так что у тебя тут не тормозит, всегда же тормозит!» я просто загадочно улыбаюсь.vintage
12.10.2017 13:02Там уже побороли неработающий JIT под iOS? Добавили поддержку Win? Понаписали кроссплатформенных компонент, не требующих писать разный код для разных платформ?
По мне так лучше кордова с css анимациями в возможностью запуска в вебе или xamarin с полноценным компилируемым языком.
parakhod
12.10.2017 13:16github.com/Microsoft/react-native-windows
Ну и давайте не будем всё-таки сравнивать контейнеры для вебаппов с полноценной платформой.vintage
12.10.2017 13:31По винде допустим ОК, хотя поддерживается сторонней компанией.
А почему бы их не посравнивать? И это вы RN сейчас полноценной платформой назвали, который не более чем контейнер для JS?
parakhod
12.10.2017 13:43Ну дык а Android — унылый контейнер для Java (которая по факту бывает медленнее работает чем тот JS), где всё что должно шевелиться быстрее черепахи приходится писать под NDK, что уж мелочиться…
Разница в реализации UI, да. В RN это нативные View у мобильных платформ и окна в винде, в вебаппах это webview. Попробуйте на досуге продать это клиенту за те же деньги…
sshikov
11.10.2017 20:50-1Главная причина «лагающего UI» обычно исключительно кривые руки.
Ну да, разумеется — чьи-то кривые руки это причина большинства проблем, и не только в UI. Но тем не менее, для UI вообще характерна однопоточность (не вообще приложения, а только один поток работает с UI), и если вы посмотрите — то множество широко известных фреймворков сделаны именно так. И на то есть серьезные причины.
А кривые руки — уже последствия той сложности, которая при этом возникает.
Revertis
10.10.2017 20:05-8Почему это я не могу иметь мнение о языке, даже если я им не пользуюсь, но пользуюсь другими более 15-ти лет?
В других языках почему-то не обрезали программисту руки, а выполнение чего-то в отдельном потоке просто считается хорошим тоном.
Я работаю с UI уже не одно десятилетие, и знаю что где и как тормозит, зачем сразу унижать оппонента, если его не знаете?
П.С.: Спасибо за слив кармы.
mayorovp, Aquahawk, vassabi, justboris, parakhod.justboris
10.10.2017 20:18Вы можете иметь свое мнение, но это будет мнение рядового обывателя. Это примерно как ругать работу врачей или других специалистов, в работе которых вы не разбираетесь.
Практической пользы от таких набросов — ноль.
parakhod
10.10.2017 20:22Конечно вы можете иметь своё мнение, просто когда у вас наступит 30 лет опыта пользования, как у меня, например, ваше мнение станет гораздо спокойнее.
Кстати, я вам карму не сливал, она у меня у самого отрицательная ))
В наши времена даже спокойное мнение, не совпадающее с мнением большинства, склонно вызывать раздражение…sshikov
10.10.2017 22:28Про 30 лет — это вы пожалуй загнули :) javascript как языку всего примерно 21 год (в 1995 кажется он появился).
parakhod
10.10.2017 22:4630 лет «пользования другими языками».
В сентябре 1987 года впервые сел за чудовище под названием Агат-7, и начал пытаться писать что-то на его жутковатом бейсике.
Так что ровно 30 ))
На жабоскрипте же первые простенькие скриптики написал в 96 где-то, когда забацал свой homepage на geocities. Правда последующие 17 лет им не пользовался — других забот хватало…
mayorovp
10.10.2017 20:27+3Карму вам не сливал… до этого вашего комментария. Почему когда у людей кончаются аргументы — они начинают вопить о сливе кармы? Как будто карма — аргумент.
TheShock
10.10.2017 20:50+1П.С.: Спасибо за слив кармы.
Круто, вы — читер, знаете, кто вам карму сливает??
PaulZi
10.10.2017 19:07-1Ох уж этот JavaScript… Сначала придумаем Promise, потом надстроим над ним сахар в виде async/await. В итоге за внешней простотой кода скрывается куча сложностей под капотом, что неизбежно приводит к побочным явлениям и неожиданным результатам.
О чём, собственно, я пытаюсь порассуждать вслух. О том, что история в какой-то момент пошла не туда, и убогий язык стал мэйнстримом, который стали обвешивать костылями. А ведь чем проще решение, тем стабильнее оно работает.mayorovp
10.10.2017 20:36+2Монада Promise — это общий подход, который сейчас применяется в языках C#, Python, Java (тут пока без синтаксиса async/await), хотят ввести в С++. Видел библиотеку и для Ruby, но не уверен насчет популярности.
В любом случае, из известных развивающихся языков в стороне от этого подхода остались лишь Go с его девизом "программист должен страдать" и Haskell с его ленивыми вычислениями. Вы точно уверены что обещания — это костыли, а не новая парадигма асинхронного программирования?
sshikov
10.10.2017 22:17В смысле — старая парадигма? Насколько я помню, этой парадигме лет 10 уже наверное минуло (не в виде async/await, а в изначальном).
dimka11
10.10.2017 22:01-4Почему нельзя было, просто добавить блокировку IO в язык?!
sshikov
10.10.2017 22:25Можете пояснить, как это можно было сделать "просто"? Ну, так чтобы не сломать все, что было до этого?
vintage
11.10.2017 04:12-1sshikov
11.10.2017 20:36Вы считаете, что это просто? Во-первых, что там в браузере? А во-вторых, сломать таки можно многое. Ну хотя бы потому (на самый первый взгляд), что нужна будет какая-никакая синхронизация. По-моему они в браузере особо и не приживаются как раз по этой причине.
vintage
11.10.2017 21:52Да, с ним всё куда проще, чем с async/await.
В браузере никак ибо не стандарт.
Да нет, там всё в одном потоке исполняется, никакой особой синхронизации не нужно.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()
musuk
11.10.2017 04:05Попробовал я один AngularJS-проект с промисов на async/await переписать, еще всякие let, const, и стрелочные функции использовать. Babel настроил, как мог.
Получилось, конечно, красиво. Только вот у AngularJS свои собственные промисы, которые умеют делать $apply(). Можно, конечно windows.Promise переопределить, но у меня на проекте есть куча разных сторонних библиотек не связанных с ангуляром. Так что пришлось вызывать $scope.$apply() явно.
Стали очень плохо работать брякпоинты в Chrome Developer Console. Причем заметил я это, когда большая часть проекта уже была переписана. Мучал я babel, читал форумы, но как я понял, с отладкой es7 кода на браузере как-то все не очень радужно.mayorovp
11.10.2017 06:12Хром давно умеет async/await нативно, в дев-сборке надо лишние плагины в babel по-отключать было.
А проблема своих промисов Ангуляра решается использованием генераторов обернутых в интерпретатор вместо асинхронных функций. Вот решение аналогичной проблемы в mobx-utils: https://github.com/mobxjs/mobx-utils/blob/master/src/async-action.ts
MishUshakov
11.10.2017 14:15Интересная тема, но насколько она практичная?
wert_lex
11.10.2017 19:50Более чем практичная. Проект на 75kloc — сначала перевели тесты через бабель еще во времена 6-й ноды (~33kloc). Потом перевели всё остальное уже с 8-й нодой. Все довольны очень сильно, всё здорово.
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}/`); });
mayorovp
14.10.2017 14:31Мне кажется, тут один уровень вложенности лишний: можно либо
async
выше перенести (http.createServer(async (req, res) => { ... })
либо try-catch ((async () => { ... })().catch(e => { ... })
)
И я бы еще проверял результат вызова
write
и ждал событияdrain
если вернулось false.MaZaAa
14.10.2017 14:49res.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, в любом случае данные либо сразу ушли, либо встали в очередь в памяти на оправку и все равно уйдут, если конечно никаких внезапных крэшей не произойдет)mayorovp
14.10.2017 16:31Да, но если продолжать писать в res когда буфер забит — он будет неограниченно расширяться, что приведет к перерасходу памяти. Для небольших примеров это нормально, но в общем случае лучше бы приостановить генерацию контента когда буфер забился.
MaZaAa
14.10.2017 17:03-1Проблема не актуальная, т.к. легко решается масштабированием.
Другой вопрос, если денег на масштабирование нету, а нагрузка большая, то тут уже надо не выдумывать костыли на ноде, а на уровне операционной системы ограничивать кол-во соединений с вашим сервером, но тогда будут «лишние» недовольные пользователи, чьи запросы ваш сервер будет отклонять, просто потому, что физически их не сможет обслужить.mayorovp
14.10.2017 17:10Если бы проблема была неактуальной и решалась бы масштабированием — то события drain никто бы не вводил.
MaZaAa
14.10.2017 17:15То есть вы хотите сказать, что масштабирование на 100% не решает эту проблему?
mayorovp
14.10.2017 17:16+1Конечно же не решает. Хакеру пофиг сколько серверов ложить атакой медленного чтения.
MaZaAa
14.10.2017 17:29Так ваше решение не спасает от этого =) Так или иначе пользователи не будут получать ответ от сервера.
От такой атаки нужно защищаться по другому. Например установить таймаут на все соединения, например если в течении 3-5 секунд соединение все ещё висит, то обрубаем его. И если с этого айпишника приходит более N таких соединений за N время, то вообще блокируем его на N часов.mayorovp
14.10.2017 17:36Нет, мое решение от этого как раз спасает, потому что ограничивает используемые соединением ресурсы. И как раз после этого все эти решения по ограничению числа соединений становятся эффективны.
MaZaAa
14.10.2017 18:00Я надеюсь вы понимаете, что в случае настоящей DDoS атаки, а не баловства ляжет 99.9% всех проектов. Поэтому не нужно параноить и создавать себе иллюзии, что ваш VDS с 1 гигом оперативы и один ядром процессора, становится неуязвимым против атак, если вы проверяете событие drain)
mayorovp
14.10.2017 18:01Тем не менее, есть некоторая разница между сервером который может положить любой школьник и сервером который может положить не любой школьник.
MaZaAa
14.10.2017 18:06Школьник способный положить хоть что-то, уже не любой школьник.
Тем более нужно ещё постараться сделать что-то, чтобы кто-то об этом узнал и у него в принципе возникло желание положить это что-то.
mayorovp
Оставлю альтернативный вариант распараллеливания для тех случаев когда не получается сделать красиво через
Promise.all
:Iqorek
Красивше будет
Так имхо читабельней.
justboris
Тогда уж и до такого недалеко
Кстати, mayorovp, а чем вам этот вариант не подошел?
mayorovp
Конкретно в данном случае он подходит. Но так бывает не всегда.
Сталкивался пару раз с ситуацией, когда надо в старом коде запустить параллельный процесс. Переводить на
Promise.all
в таком случае означает изменить 30 строк, что выльется в приключения с git rebase перед пушем если кто-то еще правил этот метод и затруднит git blame в следующие пять лет поскольку сделает меня автором строк которые я не писал.А альтернативный подход — это всего 2 измененные строки.
dfuse
А я вот считаю, что лучше тронуть 30 строк и написать новое красивое решение (которым можно гордиться ближайшие 5 лет), чем костылить 2 и получать бяку в итоге :)
mayorovp
А я вот не считаю использование
Promise.all
настолько сложным чтобы его использованием можно было гордиться 5 лет...Для рефакторинга же придет время когда я останусь один на проекте :-)
ingumsky
При таком подходе печалит количество констант, которые требуются один раз, но для каждой из которых приходится придумывать имена :(
Chamie
Так не заводите констант. Просто по месту использования приписывайте
await
перед переменной с промисом.YemSalat
По-моему уж лучше через Promise.all, а то уж больно неочевидно получается, и ошибок так проще наделать.
Svan
Есть ещё вариант.
mayorovp
Нет, так работать не будет. То есть конкретно для `console.log` это сработает — но ведь в реальной программе эти запросы не для выдачи в лог делаются…
Svan
Что именно работать не будет?
mayorovp
То, что вы написали, работать не будет.
Оно выведет что-то типа
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }} Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: { ... }}
вместо данных двух пользователей.Svan
Странно, мне выводит данные двух пользователей.
mayorovp
От реализации консоли зависит, сколько уровней вложенности она показывает. Но в переменных user1 и user2 от этого не начинают храниться пользователи вместо обещаний.
Svan
Вы правы.
Svan
…
a = await a; b = await b;
можно типа такого. Хотя так уже мне самому не особо нравится.