В конце 2015 года я услышал об этой паре ключевых слов, которые ворвались в мир JavaScript, чтобы спасти нас от promise chain hell, который, в свою очередь, должен был спасти нас от callback hell. Давайте посмотрим несколько примеров, чтобы понять, как мы дошли до async/await.
Допустим, мы работаем над нашим API и должны отвечать на запросы серией асинхронных операций:
— проверить валидность пользователя
— собрать данные из базы данных
— получить данные от внешнего сервиса
— изменить и записать данные обратно в базу данных
Также давайте предположим, что мы не имеем каких-либо знаний о промисах, потому что мы путешествуем назад во времени и используем функции обратного вызова, чтобы обработать запрос. Решение выглядело бы примерно так:
function handleRequestCallbacks(req, res) {
var user = req.user
isUserValid(user, function (err) {
if (err) {
res.error('An error ocurred!')
return
}
getUserData(user, function (err, data) {
if (err) {
res.error('An error ocurred!')
return
}
getRate('service', function (err, rate) {
if (err) {
res.error('An error ocurred!')
return
}
const newData = updateData(data, rate)
updateUserData(user, newData, function (err, savedData) {
if (err) {
res.error('An error ocurred!')
return
}
res.send(savedData)
})
})
})
})
}
И это так называемый callback hell. Теперь вы знакомы с ним. Все его ненавидят, так как его трудно читать, отлаживать, изменять; он уходит все глубже и глубже во вложенности, обработка ошибок повторяется на каждому уровне и т.д.
Мы могли бы использовать знаменитую async библиотеку, чтобы немного очистить код. Код стал бы лучше, так как обработка ошибок, по крайней мере, была бы в одном месте:
function handleRequestAsync(req, res) {
var user = req.user
async.waterfall([
async.apply(isUserValid, user),
async.apply(async.parallel, {
data: async.apply(getUserData, user),
rate: async.apply(getRate, 'service')
}),
function (results, callback) {
const newData = updateData(results.data, results.rate)
updateUserData(user, newData, callback)
}
], function (err, data) {
if (err) {
res.error('An error ocurred!')
return
}
res.send(data)
})
}
Позже мы узнали как использовать промисы и подумали, что мир больше не злится на нас; мы почувствовали, что нужно провести рефакторинг кода еще раз, ведь все больше и больше библиотек также движется в мир промисов.
function handleRequestPromises(req, res) {
var user = req.user
isUserValidAsync(user).then(function () {
return Promise.all([
getUserDataAsync(user),
getRateAsync('service')
])
}).then(function (results) {
const newData = updateData(results[0], results[1])
return updateUserDataAsync(user, newData)
}).then(function (data) {
res.send(data)
}).catch(function () {
res.error('An error ocurred!')
})
}
Это гораздо лучше, чем раньше, гораздо короче и намного чище! Тем не менее возникло слишком много накладных расходов в виде множества then() вызовов, function () {...} блоков и необходимости добавлять несколько операторов return повсюду.
И наконец мы слышим о ES6, обо всех этих новых вещах, которые пришли в JavaScript: например стрелочные функции (и немного деструктуризации, чтобы было чуть веселее). Мы решаем дать нашему прекрасному коду еще один шанс.
function handleRequestArrows(req, res) {
const { user } = req
isUserValidAsync(user)
.then(() => Promise.all([getUserDataAsync(user), getRateAsync('service')]))
.then(([data, rate]) => updateUserDataAsync(user, updateData(data, rate)))
.then(data => res.send(data))
.catch(() => res.error('An error ocurred!'))
}
И вот оно! Этот обработчик запросов стал чистым, легкочитаемым. Мы понимаем что его легко изменить если нам нужно добавить, удалить или поменять местами что-то в потоке! Мы сформировали цепочку функций, которые одна за другой мутируют данные, которые мы собираем с помощью различных асинхронных операций. Мы не определяли промежуточные переменные для хранения этого состояния, а обработка ошибок находится в одном понятном месте. Теперь мы уверены, что определенно достигли JavaScript высот! Или еще нет?
И приходит async/await
Несколько месяцев спустя async/await выходит на сцену. Он собирался попасть в спецификацию ES7, затем идею отложили, но т.к. есть Babel, мы прыгнули в поезд. Мы узнали, что можем отметить функцию как асинхронную и что это ключевое слово позволит нам внутри функции «остановить» ее поток исполнения до тех пор, пока промис не решит, что наш код снова выглядит синхронным. Кроме того, функция async всегда будет возвращать промис, и мы можем использовать try/catch блоки для обработки ошибок.
Не слишком уверенные в пользе, мы даем нашему коду новый шанс и идем на окончательную реорганизацию.
async function asyncHandleRequest(req, res) {
try {
const { user } = req
await isUserValidAsync(user)
const [data, rate] = await Promise.all([getUserDataAsync(user), getRateAsync('service')])
const savedData = await updateUserDataAsync(user, updateData(data, rate))
res.send(savedData)
} catch (err) {
res.error('An error ocurred!')
}
}
И теперь код снова выглядит как старый обычный императивный синхронный код. Жизнь продолжилась как обычно, но что-то глубоко в вашей голове говорит нам что что-то здесь не так…
Функциональная парадигма программирования
Хотя функциональное программирование было вокруг нас в течение более чем 40 лет, похоже что совсем недавно парадигма стала набирать обороты. И лишь в последнее время мы стали понимать преимущества функционального подхода.
Мы начинаем обучение некоторым из его принципов. Изучаем новые слова, такие как функторы, монады, моноиды — и вдруг наши dev-друзья начинают считать нас крутыми, потому что мы используем эти странные слова довольно часто!
Мы продолжаем свое плавание в море функциональной парадигмы программирования и начинаем видеть ее реальную ценность. Эти сторонники функционального программирования не были просто сумасшедшими. Они были, возможно, правы!
Мы понимаем преимущества неизменяемости, чтобы не хранить и не мутировать состояние, чтобы создать сложную логику путем объединения простых функций, чтобы избежать управления циклами и чтобы вся магия была сделана самим интерпретатором языка, чтобы мы могли сосредоточиться на том, что действительно важно, чтобы избежать ветвления и обработки ошибок, просто комбинируя больше функций.
Но… постойте!
Мы видели все эти функциональные модели в прошлом. Мы помним, как мы использовали обещания и как соединяли функциональные преобразования один за другим без необходимости управлять состоянием или ветвить наш код или управлять ошибками в императивном стиле. Мы уже использовали promise-монаду в прошлом со всеми сопутствующими преимуществами, но в то время мы просто не знали это слово!
И мы вдруг понимаем, почему код на основе async/await смотрелся странно. Ведь мы писали обычный императивный код, как в 80-х годах; обрабатывали ошибки с try/catch, как в 90-х годах; управляли внутренним состоянием и переменными, делая асинхронные операции с помощью кода, который выглядит как синхронный, но который внезапно останавливается, а затем автоматически продолжает выполнение когда асинхронная операция будет завершена (когнитивный диссонанс?).
Последние мысли
Не поймите меня неправильно, async/await не является источником всего зла в мире. Я на самом деле научился любить его после нескольких месяцев использования. Если вы чувствуете себя комфортно, когда пишете императивный код, научиться использовать async/await для управления асинхронными операциями может быть хорошим ходом.
Но если вы любите промисы и хотите научиться применять все больше и больше функциональных принципов программирования, вы можете просто пропустить async/await, перестать думать императивно и перейти к новой-старой функциональной парадигме.
См. также
> Еще одно мнение о том, что async/await не такая уж хорошая вещь.
Поделиться с друзьями
impwx
Так и не понял аргументов автора. Судь
Иногда это бывает очень нужно, например для такого случая:async/await
как раз в том, чтобы сделать асинхронный код максимально похожим на обычный, чтобы его было проще понимать и легче отлаживать.Аналогичный код с promise'ами выглядит очень гадко.
VasilioRuzanni
У статьи, конечно, несколько громкий заголовок — async/await — вовсе не шаг назад, особенно если программировать в императивном стиле (а он именно для этого и появился — не все пишут в функциональном и/или реактивном стиле). Аргумент автора лишь в том, что когда привыкаешь к функциональному подходу, он сам по себе уже кажется чище и элегантнее.
У меня async/await был самой ожидаемой фичей ES-Next, пока не пришлось столкнуться с Observables :) Теперь async/await, в общем-то, не у дел, но это и не значит, что он плох.
Ну и, к слову, несмотря на то, что код выглядит как синхронный, он именно что только «выглядит» так — асинхронность все равно надо держать в уме, и, поскольку await разворачивается в Promise, все равно нужно понимать, как именно он это делает.
Fen1kz
Ну… но я привы… ну ла… Не, стоп, но зато явная асинхронность и можно одновременно посылать несколько запросов + отменять их (с тем же bluebird).
Ну то есть ваш пример вырожден. Добавьте ту же обработку ошибок, и у дела уже хуже.
zekohina
А такое?
justboris
Даже проще.
А еще вы в курсе, что ваш код отсылает запросы последовательно, один за одним?
А если нужно параллельно?
а на промисах — раз и готово
byme
Паралельно и короче…
justboris
А что такое
asParallel()
?В формулировке "проще", я имел в виду, что не нужно подключать Babel или колдовать с флагами в Node.js.
Если async функции будут поддерживаться без дополнительных проблем, то мой вариант конечно же не проще.
raveclassic
Если мне не изменяет память, конструкция asparallel/select — это rxjs. Круто. Притащили rx.
byme
Вот мы и подхошли к мысли, что все подходы хороши, но при одном условии, если их использовать в нужных местах=)
raveclassic
byme
А что плохого в том что бы использовать несколько подходов? Религия не позволяет?
raveclassic
Если вы сейчас про .Net, то странно его упоминать в контексте обсуждения «async/await или Promise в JS».
Если вы все же про JS, то зачем «несколько подходов» для стандартного способа распараллеливания и сбора результатов
Promise.all
? Уже дажеawait*
задеприкейтили в пользу последнего.byme
По сути map который возвращет таски, которые инкапсулируют елементы массива и позволяют их обрабатывать парелельно. Я его так назвал по аналогии с миром .Net где async/await єсть уже относительно давно и метод AsParallel стал дополнением стандартной библиотеки. Имя может быть другим не в этом суть. Суть том что вместе с раширением возможностей языка будут расширены и возможности библиотек, не стоит об этом забывать.
justboris
На ваш код Babel ругается:
https://babeljs.io/repl/#?babili=false&evaluate=true&lineWrap=false&presets=es2015%2Ces2016%2Ces2017%2Cstage-3&code=async%20function%20fetchAll(urls)%20%7B%0A%20%20%20%20return%20urls.asParalell().select(url%20%3D%3E%20await%20fetch(url))%3B%0A%7D
И он прав, потому что async написан у корневой функции, а await внутри другой, которая идет аргументом в map. Async так не работает.
zekohina
Так ведь async/await и не отменяет промисы. Там где нужна параллельность, действительно можно использовать Promise.all(), но в моем примере нужна была именно последовательность.
TheShock
Почему вы считаете, что такой код проще такого? Зачем reduce, который усложняет понимание кода?
justboris
Кому-то больше нравится писать код с for-циклами, а кто-то больше любит методы массива.
Тут уже личный выбор каждого. Я предпочитаю, не бегать по массиву через for, а использовать filter/map/reduce, но это мое личное предпочтение.
kana-desu
Редьюс будет все же проще, я вам как хаскелист говорю
TheShock
В Хаскеле? Возможно)
DistortNeo
async/await — просто синтаксичский сахар для Promise, не вижу причин спорить
vintage
Вариант с атомами:
justboris
Вроде же изначально договорились никакие библиотеки не использовать.
Так-то я могу тоже взять гипотетическую библиотеку
url-fetcher
, которая делает то, что мне нужно, надо лишь ее вызвать.vintage
Вы видимо тайком договаривались. :-)
В том-то и дело, то атомы не "делают то, что вам нужно". Они лишь позволяют синхронный код исполнять асинхронно:
justboris
Не спорю, что с атомами, как и с rx, async.js и другими библиотеками код будет короче чем без них.
Но вообще это комментарии к статье о том, нужен ли async/await в стандарте языка. Библиотеки здесь не при чем.
vintage
А при чём тут библиотеки? Я говорю о концепции. И концепция писать синхронный, но не блокирующий код — нужна.
DistortNeo
Но есть нюанс: вызов
urls.reduce
приведёт к созданию огромного объектаPromise.resolve().then(...).then(...).then(...).then(...).then(...).then(...)....
,тогда как код с async/await будет генерить промисы по мере необходимости.
В реальной ситуации это, конечно, не играет никакого значения — вряд ли вы будете по 100000 промисов отрабатывать за раз.
justboris
А чем именно плоха большая цепочка then?
Зато можно быть уверенным, что пройдутся все урлы, а не потеряется что-то в процессе, если кто-то снаружи смутирует массив
urls
. Вот пример. Очень невероятный пример, но все же.DistortNeo
Точно так же можно смутировать и элементы массива
urls
, если они представляют собой объекты, не строчки. Ещё более невероятная ситация, но тоже возможна.justboris
Хорошо, а что с ответом на вопрос:
TheShock
Мне вообще этот подход с промисами кажется странным и необходимость в них (а уж тем более такая расширенная) попахивает плохой архитектурой. У вас есть данные. Вы заносите их в модель (можно даже паралельно) и когда данных в модели приложения достаточно — выполняете действие.
Т.Е. у нас есть модуль обновления модели:
А также есть модуль (никак не связанный с загрузкой моделей), который отвечает за выполнение какого-либо действия:
Мы к этому подходу пришли задолго до бума промисов и опробовали его на огромной игре — когда отказались от прямой подписки на события («загрузи и сделай это»), то сложность приложения значительно снизилась, а «колбек-хел» забылся как страшный сон.
Потому не могу понять, почему с ними до сих пор носятся так сильно.
Чем плоха большая цепочка then? Да потому что что угодно может произойти, пока она выполняется. А еще нельзя показать промежуточный уже загруженный результат. А ещё нельзя не выполнить колбек (если пользователь ушел с этого экрана). И вообще ад колбеков никуда не девается.
DistortNeo
Просто создаётся слишком тяжёлый объект, но я это проблемой не считаю.
Вот более сложный пример для перевода в промисы:
Fen1kz
Вообще плохо что я повелся и написал это так, ваша функция грязная и пахнет =(
Из контекста приходят: timeOut, delay (зачем брать из контекста delay и из него же timeOut?), isGood. Тестировать такую функцию почти невозможно =/
raveclassic
Вы серьезно считаете, что это читабельней?
TheShock
Вы ведь знаете что я хочу сказать, да?
Плевать на нечитаемость! Зато другие хипстеры на афтерпати будут восхищены!
Fen1kz
Да успокойся ты, глупый хипстерофоб. Я же приписал в конце — что функция убогая и не тестируемая сама по себе, так что я ошибся когда вообще попытался написать ее "красиво"
TheShock
Я глупый? Ну-ну. Прекрасная попытка оскорбления! Сразу показывает ваш уровень и силу аргументов. Теперь то очевидно, что вы были правы и промисы значительно лучше async-await, спасибо, что открыли мне глаза.
А если серьезно, то раз так сложно в таком стиле написать хорошо, то может не выпендриваться и просто писать хороший код в классическом стиле, а не стараться угнаться за сиюминутной модой?
Fen1kz
Давайте так, я пытался и у меня не получилось, ок?
justboris
Согласен, время timeout можно передать в аргументах. А в утилитарной функции delay не вижу ничего плохого. Удобнее, чем каждый раз new Promise разворачивать
TheShock
delay — библиотечная функция, timeOut — константа в проекте. А как иначе?
Ваш код стремится к нечитаемости
reforms
Ваш пример еще можно уменьшить
Хотя Ваш вариант (await) мне нравится куда больше.
abby
Это к чему? Тут как раз, скорее всего, async-await не нужен, потому что, скорее всего, нет смысла обрабатывать URL'ы по очереди, было бы лучше «одновременно».
zekohina
Спасибо, я знаю про то что их можно запустить параллельно.
Но смысл есть, когда количество урлов идет на тысячи.
abby
Когда на тысячи я использую хак (а может так и надо?) в виде пула, но я полностью за Ваш вариант с async-await по сравнению с примером выше с reduce, если нужно именно последовательно.
justboris
Ограничивать число одновременных запросов можно на уровне модуля http.
Зачем засорять логику приложения такими подробностями?
Fen1kz
Вот видите, уже не так красиво и прозрачно, например вот вы в своем примере ошиблись.
zekohina
Где ошибся?
Fen1kz
Ну, я написал "можно одновременно посылать несколько запросов"
А у вас последовательные. Это не одновременно.
TheShock
"Async/await плохо, потому что это императивно, а значит не модно, а нас ведь считают крутыми, потому что мы пишем на ФП."
Без шуток. Это аргументация профессиональных программистов. И они правда так думают.
potan
У нас, так исторически сложилось, что тесты написаны асинхронно. Правда на Scala, но сути это не меняет.
При этом используются целых три стиля — flatMap (на сколько я понимаю, это в js называется «через callback»), с помощью for (в js аналога, по моему, нет, похоже на do в Haskell), и async/await.
Для меня с flatMap единственное неудобство — большой уровень вложенности синтаксических конструкций (соответственно проблема, как поступать с отступами). for выглядит чище, я последнее время склоняюсь к нему. Стиль async/await имеет много фанатов, но мне от показался малопонятным (я давно программирую на функциональных языках и более императивный код мне читать сложнее). Кроме того, в этом коде очень легко где-нибудь забыть написать await на какую-нибудь фьючу, результат которой игнорируется (в нашем случае это обычно было удаление созданных для теста сущностей), и получить race condition.
В общем, async/await мне не понравился.
Jabher
staticlab
Можно ведь разработать proposal.
Jabher
А смысл? сделали же уже async/await. Много разных API это не хорошо.
А асинки уже вошли в es2017, если не ошибаюсь.
TheShock
Скорее не крутыми, а смешными.
О нет! Если я напишу более читаемый и поддерживаемый код, то современные хипстеры меня засмеют, ибо это уже не модно и вообще, так писали в 90-х, какой позор!
Статью писал сектант. «Не юзайте асинк-евейт, т.к. это богомерзкая императивщина, а свидетели ФП не используют ничего из мира императивщины, даже если это значительно удешевит поддержку кода». Фу.
И снова не крутые, а смешные, как и обычно.
Кстати, ваш священный Редакс тоже написан в императивном стиле. Видимо, чтобы продавать веру простому люду не обязательно самому в это верить:
raveclassic
TheShock
О чем вы? Я ведь процитировал предложение из топика по поводу которого была моя ирония. Или вы увидели только то, что хотели?
Процитирую лично для вас, Этот текст написан в статье:
Так что ваш вопрос должен быть адресован к автору топика
raveclassic
Нет, просто рвение, с которым вы постоянно жалуетесь на вездесущих хипстеров, вводит в недоумение, если честно. С мыслью-то я согласен, неудачно уловил контекст.
TheShock
Я могу объяснить. Вот человек не определился, заходит в такую статью и начинает ей верить, т.к. не имеет достаточно контекста.
Если не высмеивать подобные творения, то получим еще одного программиста, который судит категориями «это круто» / «это модно» / «надо отказываться от ооп в пользу фп» а то и начинает тоже писать такие статьи. Если бы наша среда была крайне конкурентной, то может оно было бы и хорошо, но, к сожалению, сейчас очень трудно найти себе в команду хороших программистов.
raveclassic
Ну вы же понимаете, что хороший программист не получится из чужих советов. Давайте вспомним себя, когда мы все с остервенением педалили собственные фреймворки, реализации классов на js, библиотеки для дат, да чего там только не было, и так педалили, что дальше носа ничего не видели. Ну как? :)
Горькая правдаВот и тут, хорошим программист станет тогда, когда сам наберется опыта, лично наступив на каждую граблю. Я понимаю вашу усталось от всего этого фанбойства, но с этим ничего не поделать.
TheShock
Ну на самом деле вы зря недооцениваете влияние подобных статей и комментариев на начинающих программистов. Оно огромно.
Да, педалили, набирались опыта, нас критиковали, на какую-то критику обижались, какая-то была слишком аргументированной, чтобы ее не слышать.
Лично я очень много брал именно из комментариев. Статья на какую-либо тему, а в комментариях в пределах аргументированного спора ты видишь две противоположные мысли. Они между собой спорят, не хотят уступить, не слышат аргументов друг-друга, но ты то когда новичок и читаешь это — не имеет реально сформированного мнения, потому слышишь аргументы одной и другой стороны. С какими-то не согласен, какие-то вообще не понимаешь, т.к. не имеешь опыта, какие-то ошибочные. Но это — яркий способ понять вещи, которые в книжке описано намного хуже.
Вот спорит сторонник фреймворка А и фреймворка Б, каждый говорит, что его лучше, а другой хуже. А ты читаешь и узнаешь, вот, оказывается, если смотреть на фреймворк А под таким углом, то он и ничего вообще-то, а вот эта фича в фреймворке Б оказывается так используется.
TheShock
Судя по оповещению, вы еще спрашивали:
Да много чего.
— Смешно то, что «императивный подход — это девяностые, а функциональный — это десятые».
— А еще, что это считается важным аргументом в технической статье.
— Что думают, что пишут на JS в функциональном стиле, а в коде не имеют ни чистоты ни декларативности
— Что используют алгоритм квадратичной сложности, где более читаемый и очевидный алгоритм линейной сложности просто чтобы код казался функциональным.
— Потому что мерилом качества кода выступает не «мы поддерживаем огромный продукт уже 3 года, но все-равно с легкостью и минимум багов вносим в него функционал», а «наши dev-друзья начинают считать нас крутыми, потому что мы говорим эти странные слова довольно часто!»
— Смешна не функциональная парадигма, она очень крута в определенных областях, смешны люди, которые ее продвигают подобными статьями.
raveclassic
Да я что-то задумался, и подумал, что вы это про термины теории категорий. Ну и удалил, когда перечитал внимательней.
kernel72
Считаю, все эти парадигмы довольно условными и личными для каждого. Отдавать преимущество только async/await или только Promise, только потому что оно не удовлетворяет какой то парадигме на мой взгляд довольно странно. Не понимаю, почему async/await шаг назад.
Source
Мне кажется, имелось в виду, что явное в данном случае лучше неявного. И если писать асинхронный код, делая вид, что он синхронный, то рано или поздно начнётся танец по граблям.
DistortNeo
Программисты C# уже давно пишут асинхронный код с использованием async/await и не испытывают никаких проблем с граблями. Более того, код с использованием async/await получается более производительный, чем основанный на использовании
Task.ContinueWith
(аналог JSPromise.then
).Source
С чего Вы так решили? Ещё как испытывают и часть из этих проблем даже официально признана и решается последующими релизами C# 6, C# 7, etc.
В принципе, тут невозможно сделать недырявую абстракцию, т.к. код по факту асинхронный и попытки это замаскировать, прикинувшись, что он как бы такой же как синхронный, ни к чему хорошему не ведут. Просто чем дальше, тем более изощрёнее будут грабли.
DistortNeo
На грабли наступают, как мне кажется, те, кто решил заняться асинхронным программированием, не имея опыта многопоточного программирования, где граблей ещё больше. А синхронный многопоточный код в однопоточный асинхронный переводится очень просто и безболезненно.
Что касается C# 6.0: просто ввели возможность написания await в catch и finally блоках, что ещё больше сделало асинхронный код похожим на синхронный. В C# 7.0 существенных изменений больше нет.
Source
В C# 7 — "Generalized async return types", в каком-нибудь C# 8 ещё с out-параметрами накостылят или ещё что-нибудь. В принципе, о том и речь, что асинхронный код, похожий на синхронный, будет всегда плохим решением, потому что по факту он несинхронный. И аргумент, что те, кто хорошо понимает что происходит за сценой, не испытывают проблем — не работает. Потому что вся эта декорация делается не для них, а для тех, кто ещё толком не понимает… типа "не задумывайся, что код больше не синхронный, просто добавь
водыasync/await".staticlab
То есть опытным программистам не нужен сахар?
Source
Сахар сахару рознь… Хороший сахар делает явное ещё более явным, плохой — явное делает неявным.
DistortNeo
Generalized async return types в C# я уже пощупал — фигня это, правда, тем более, что они все равно на Task завязаны. Прироста в производительности по сравнению с обычным Task вообще нет — основной оверхед там от state machine идёт. Теоретически, польза от этого есть при полном переписывании штатного TPL на свой, с блекджеком и шлюхами.
Source
Так я и не имел в виду, что будут какие-то действительно полезные или масштабные доработки. Это скорее затыкание дыр в абстракции "асинхронный код, похожий на синхронный", которая дырява by design.
vintage
Наоборот. Асинхронный код — костыль для многозадачности в рамках одного потока. В нормальных языках есть потоки и/или сопрограммы. И те и другие — синхронны.
DistortNeo
Так и есть. В чём принципиальная разница между сотней асинхронных задач и сотней потоков, ожидающих выполнения? Да ни в чём. Просто механизмы синхронизации на уровне операционной системы дорогие, вот и приходится выкручиваться с помощью кооперативной многозадачности в юзерспейсе.
lega
vintage
В том, что потоков будет не сотня, а на порядок меньше. Сопрограмм (которые могут жить и на одном потоке) тоже будет на порядок меньше. И только асинхронное программирование требует разбиения бизнес задачи на множество мелких асинхронно исполняющихся технических подзадач вида "обработать ответ от такого-то апи и вызвать следующее".
Cryvage
В этом случае попытки замаскировать асинхронный код под синхронный проблему не решают. Если асинхронный код — это костыль, то async/await это костыль для другого костыля. Настоящий kostyl-driven design во всей своей красе.
vintage
Конечно костыль, поэтому мы с помощью реактивности абстрагируемся от (а)синхронности.
Cryvage
Майкрософт вообще частенько грешит подобным подходом. Ту же самую ошибку они допустили, когда выкатывали Web Forms, в котором пытались сделать разработку web приложений похожей на Windows Forms. Естественно «не взлетело». Вместо этого «взлетели» те походы, которые не скрывали особенности web разработки, а наоборот подчёркивали их и учитывали в своей архитектуре. За счёт этого им удалось сделать web разработку более простой и удобной.
Source
O, да! Web Forms — это был гораздо более крутой изврат… Я помню фееричный анонс UpdatePanel (для тех, кто не в теме, это такой компонент на форму внутри которого автоматически AJAX типа работает) от MS-евангелиста, вот это был реальный треш и угар.
aikixd
Асинки не параллельные. Код все равно последовательный, просто код не виснет когда ожидает ответа.
abby
Я совсем не эксперт в функциональном программировании и в async-await в JS, но, на мой взгляд, пример в статье совсем не отражает её суть.
В частности, практически каждый метод имеет побочные эффекты, любой метод может изменить внутреннее состояние аргумента, и, в общем-то, часто именно эти явления и ожидаются. То есть, идеологически полный провал.
Кстати, `isUserValidAsync` было бы лучше назвать assertValidUserAsync или использовать по-другому.
С другой стороны, в случае использования async-await, на мой взгляд, существенно улучшается читабельность кода от чего напрямую зависит его качество и стоимость поддержки. Хотелось бы заметить, что читабельность улучшается за счет меньшего количества всяких скобочек, запятых, вызываемых методов и вообще меньше кода.
При этом никто не запрещает использовать элементы функционального подхода, только без лишних ограничений на способ выражения желаемого. К тому же, наверняка, JS-движок следующей версии сможет существенно оптимальнее представить внутри себя async-await конструкции, чем свалку promise'ов, а программист все так же может продолжать использовать более менее простую модель с выворачиванием этого всего в promise'ы.
reforms
К сожалению ничего не могу сказать по поводу async/await, но вот при использовании Promise в продакшене столкнулись с такой проблемой, что стали появляться синхронно-асинхронные функции:
из-за чего лезут нехорошие баги. Хотя это не проблема Promise, а их некорректное использование.
На счет статьи показалось, что автор поведал нам хронологическую ленту развития JS
JSmitty
Попахивающий код. Сигнатура функции вообще произвольная (тип возвращаемого не определен)? Что мешает всегда возвращать промис (Promise.reject() например)?
zekohina
Код вполне типичный. Просто во всех местах где будет юзаться эта функция делается что-то типа такого:
reforms
Да такие ветки имеются, и даже такие Promise.resolve(doSomeWork(request))
atd
Как раз в таком примере as/aw вас спасёт: любая async-функция всегда вернёт промис
reforms
Promise.resolve вы наверное имели в виду? Да ничего и про сигнатуру Вы правы, я просто делюсь что имеем по факту.
vintage
Не так, вы забыли позаворачивать все хендлеры в try-catch-и, иначе любая ошибка убьёт процесс.
Maiami
Их не надо заворачивать в try-catch, потому что верхний try-catch поймает все исключения ниже
vintage
Речь о коде на колбяках. Один try-catch ничего не поймает.
Lalartu
Ну на самом деле async/await скорее можно назвать синтаксическим сахаром для CPS, так что обвинения в императивности, как минимум, выглядят голословными. Как инструмент async/await довольно удобен, проблема опять в том, как его использовать.
Mithgol
Я сейчас скажу довольно мрачную и пессимистическую вещь, отрицающую плоды прогресса даже сильнее, чем вышепереведённая мысль отрицает их.
«Promise.all», тогда как у библиотеки Async есть не только ананалогичный ему метод «async.parallel» (а также другой метод «async.waterfall», аналогичный цепному вызову промисов через их «.then»), но и ещё почти шестьдесят других удобных методов. Оговорюсь, что из них лично я использую никак не больше десятка, но, во-первых, даже этот десяток не горю желанием сочинять самостоятельно ради перехода на промисы, а во-вторых, меня радует сознание того, что за пределы этого десятка я всегда смогу (при малейшей необходимости) выйти почти мгновенно, потому что там ждут меня ещё полсотни готовых методов, которые опять же не придётся сочинять самостоятельно.
13 мая 2010 года) и большинство обретённых ею возможностей, так что теперь промисовая публика будет переизобретать велосипеды (причём, скорее всего, опять же на протяжении нескольких лет кряду) при столкновении с наималейшими проблемами действительности.
в «async.asyncify» и дальше коллбекнуть по старинке.
Мне кажется, что спохватываться надо было гораздо раньше: по сравнению с возможностями библиотеки Async даже промисы выглядят шагом назад.
Дело в том, что у промисов есть метод
То есть переход на промисы выглядит как переход на новый (и ещё не обросший возможностями) фреймворк, ради которого отбрасывается история многих лет эволюции библиотеки Async (начиная от первого коммита
То, что новый фреймворк сделали частью языка, в практическом отношении ничего особенного не меняет. (Кроме того только, что процессу переизобретения велосипедов будет сопутствовать не менее увлекательный процесс подпирания костылями-полифиллами всех тех прежних версий, которые останутся в прежних браузерах.)
Поэтому лучшее, что можно сделать с промисом — это сунуть его
Your callback hell is my home.
Maiami
Всё так и есть, не поспорить
Но async-await всё равно удобные. Удачно удалось сократить один модуль на промисах, переписав его на async-await, на 40%, и добавлять в него новый функционал стало проще и приятнее ^__^
zapolnoch
По этой логике можно вообще не развивать язык и пусть всю работу делают фреймворки. Зачем вам перебирающие методы массива (map, filter и т.д.), если у вас есть цикл for? Если нужно что-то сложнее, то используйте фреймворки, типа lodash. Собственно, именно из-за такой логики мы и находимся в ситуации, когда до 2015 года (больше чем за 15 лет своего существования), JS-стандарт обновлялся только два раза. Именно поэтому JS стал восприниматься как ассемблер на котором невозможно что-то сложное написать, а шутка про день, когда не было создано ни одного нового JS-фрейморка уже набила оскомину.
То что «фреймворк» делают частью языка меняет абсолютно все. Как минимум, можно быть уверенным что выполнение подобных задач будет оптимизировано на уровне интерпретатора и это положительно скажется на производительности.
justfly1984
По моему всё гораздо проще: Удобно использовать async/await => используем async/await. Удобно Promise писать, пишем Promise. Если код дорого переписывать на FP, оставляем императив. Это более чем Advanced javascript. Новички всё равно будут отстреливать себе ноги и с тем и с другим и с третьим. Если ты профи с FP, это не означает что ты никогда не будешь писать/обновлять OOP. Если ты катаешься по конференциям и твой твиттер полон фэнов хипстеров, это не означает что ты всегда прав. В конце концов даже Дуглас Крокфорд был неправ по поводу;
PS: и для Promise и для async/await пока без полифилов можно писать только на Node.js и прототипы в хроме канари. Сафари не поддерживает без полифилов fetch API, Edge… нет на Маках, Хрома нет на IOS Но господи как я рад что сейчас не 2009 год и это самые большие проблемы хайпа.
Babel 6, Webpack 2, React 15, ES6+ сделали жизнь на много проще.
zorro1211
Ура, нашелся оптимистично настроенный человек наконец-то =) Спасибо, подняли настроение.
LMnet
В Scala (и scala.js) существуют и промисы (только используется другой нейминг, вместо
Promise
они называютсяFuture
) и async/await. И все предпочитают использовать именно промисы (фьючи). Причина в том, что async/await создает ложную иллюзию синхронности происходящего. Это может привести к тому, что синхронная и асинхронная логика будут смешаны вместе и в какой-то момент await потеряется перед асинхронной функцией. Или наоборот, await "на всякий случай" будет приписываться везде, даже если функция не асинхронная. При использовании промисов синхронные и асинхронные потоки явно отделены друг от друга. Глазами сразу видно, где тут у нас асинхронное выполнение, а где синхронное. И это потенциально более безопасно и не приведет к ошибкам, которые я выше описал.Чтобы асинхронный код не превращался в кашу в Scala используется for-comprehension. И такой асинхронный код выглядит почти как синхронный, но при этом явно видно, где тут синхронные части, а где асинхронные.
В Scala/Scala.js коде пример выше выглядел бы примерно следующим образом:
Код вполне наглядный. Видно, что за чем идет. Если какой-то шаг вернет ошибку, то выполнение всего блока for-comprehension прервется.
vintage
И чем стрелочка и for принципиально отличаются от await и async соответственно? :-)
staticlab
Думаю, подразумевается, что в блоке for-yield не нужно await-ить каждую асинхронную операцию (соответственно нет риска забыть где-нибудь await), вдобавок при этом явно выделен асинхронный блок.
vintage
Что такое стрелочка как не await?
staticlab
В принципе да. Посмотрел, что стрелочка — это синтаксис генератора (не обратил внимания — сказался опыт R). То есть на JS действительно можно сделать то же самое, если for-yield заменить на анонимную асинхронную функцию, а стрелочки на await. В результате получим, что
LMnet
Все асинхронные функции явно вынесены внутрь for блока. В случае с async/await синхронные и асинхронные функции перемешаны на одном уровне.
staticlab
Вот обратно портировал код со Скалы на JS:
Все асинхронные функции явно вынесены внутрь блока
async () => { ... }
. Найдите 10 отличий, как говорится.LMnet
Отличие в том, что сам по себе синтаксис и конструкции языка не форсируют такой подход. Да, приведенный пример показывает, что можно явно разделить асинхронный и синхронный код и всё будет хорошо. Но это не значит что нельзя не разделять асинхронный и синхронный код. Получается страховка от ошибок на уровне договоренностей, а не на уровне конструкций языка. И именно в этом проблема.
staticlab
В Скале можно выполнить синхронный код внутри for-yield?
LMnet
Можно. Примерно так:
Но не страшно выполнять синхронный код в асинхронном блоке, это потенциально не приведет к ошибке. Страшнее выполнить асинхронный код в синхронном блоке и забыть await. Поэтому я и утверждаю, что такое разделение — это хорошо.
staticlab
Я так понимаю, что в Скале компиляция чего-то вроде
приведёт к ошибке компиляции из-за строгой типизации. В таком случае аналогично помочь может TypeScript.
LMnet
Если ты засунешь эти три строки в for-comprehension — то да, будет ошибка компиляции.
Насчет TypeScript — в нем нету аналога for-comprehension. То есть там будет такая же ситауция, как и в js.
raveclassic
Если
someSyncFun
принимает неPromise
, то, если не за-await-итьsomeAsyncFun1
, ошибка будет. Но это практически бесполезная полумера. В принципе можно еще руками проставить типresAsync1
, тогда тоже будет ошибка, но об этом тоже нужно помнить.LMnet
Да, при несовпадении типов дальше по коду можно выловить часть ошибок. Но если функция с сайд эффектом и ничего не возвращает, а нам нужно дождаться этого сайд эффекта, то мы просто получим race condition, если забыли await. В моем примере выше на scala.js даже функция без возвращаемого значения (
isUserValidAsync
) завернута в for-comprehension и явно видно, что она является частью асинхронного блока.raveclassic
Понятное дело, я и не спорю, мне такой формат записи по душе. Увы, ни JS, ни TS не предоставляют таких строгих конструкций.
staticlab
Если вы вместо
_ <- isUserValidAsync(user)
по ошибке напишетеisUserValidAsync(user)
, что будет?LMnet
Не скомпилируется. for-comprehension всегда требует что-то возвращать в левую часть стрелки. Если возвращать нечего — то используется запись
_ <- blabla