
Сегодня мы рассмотрим шесть особенностей async/await, позволяющих отнести новый подход к написанию асинхронного кода к разряду инструментов, которые стоит освоить и использовать везде, где это возможно, заменив ими то, что было раньше.
Основы async/await
Для тех, кто не знаком с async/await, вот основные вещи, которые полезно будет знать прежде чем двигаться дальше.
- Async/await — это новый способ написания асинхронного кода. Раньше подобный код писали, пользуясь коллбэками и промисами.
- Наше предложение «забыть о промисах» не означает, что они потеряли актуальность в свете новой технологии. На самом деле, в основе async/await лежат промисы. Нужно учитывать, что этот механизм нельзя использовать с коллбэками.
- Конструкции, построенные с использованием async/await, как и промисы, не блокируют главный поток выполнения программы.
- Благодаря async/await, асинхронный код становится похожим на синхронный, да и в его поведении появляются черты такого кода, весьма полезные в некоторых ситуациях, в которых промисами пользоваться было, по разным причинам, неудобно. Но раньше без них было не обойтись. Теперь же всё изменилось. Именно тут кроется мощь async/await.
Синтаксис
Представим, что у нас имеется функция
getJSON
, которая возвращает промис, при успешном разрешении которого возвращается JSON-объект. Мы хотим эту функцию вызвать, вывести в лог JSON-объект и вернуть done
.С использованием промисов подобное можно реализовать так:
const makeRequest = () =>
getJSON()
.then(data => {
console.log(data)
return "done"
})
makeRequest()
Вот как то же самое делается с использованием async/await:
const makeRequest = async () => {
console.log(await getJSON())
return "done"
}
makeRequest()
Если сопоставить два вышеприведённых фрагмента кода, можно обнаружить следующее:
- При описании функции использовано ключевое слово
async
. Ключевое словоawait
можно использовать только в функциях, определённых с использованиемasync
. Любая подобная функция неявно возвращает промис, а значением, возвращённым при разрешении этого промиса, будет то, что возвратит инструкцияreturn
, в нашем случае это строкаdone
.
- Вышесказанное подразумевает, что ключевое слово
await
нельзя использовать вне async-функций, на верхнем уровне кода.
// Эта конструкция на верхнем уровне кода работать не будет // await makeRequest() // А такая - будет makeRequest().then((result) => { // do something })
- Запись
await getJSON()
означает, что вызовconsole.log
будет ожидать разрешения промисаgetJSON()
, после чего выведет то, что возвратит функция.
Почему async/await лучше промисов?
Рассмотрим обещанные шесть преимуществ async/await перед традиционными промисами.
?1. Лаконичный и чистый код
Сравнивая два вышеприведённых примера, обратите внимание на то, насколько тот, где используется async/await, короче. И ведь речь, в данном случае, идёт о маленьких кусках кода, а если говорить о реальных программах, экономия будет ещё больше. Всё дело в том, что не нужно писать
.then
, создавать анонимную функцию для обработки ответа, или включать в код переменную с именем data
, которая нам, по сути, не нужна. Кроме того, мы избавились от вложенных конструкций. Полезность этих мелких улучшений станет заметнее, когда мы рассмотрим другие примеры.?2. Обработка ошибок
Конструкция async/await наконец сделала возможной обработку синхронных и асинхронных ошибок с использованием одного и того же механизма — старого доброго
try/catch
. В следующем примере с промисами try/catch
не обработает сбой, возникший при вызове JSON.parse
, так как он выполняется внутри промиса. Для обработки подобной ошибки нужно вызвать метод .catch()
промиса и продублировать в нём код обработки ошибок. В коде рабочего проекта обработка ошибок будет явно сложнее вызова console.log
из примера.const makeRequest = () => {
try {
getJSON()
.then(result => {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(result)
console.log(data)
})
// раскомментируйте этот блок для обработки асинхронных ошибок
// .catch((err) => {
// console.log(err)
// })
} catch (err) {
console.log(err)
}
Вот то же самое, переписанное с использованием async/await. Блок
catch
теперь будет обрабатывать и ошибки, возникшие при парсинге JSON.const makeRequest = async () => {
try {
// парсинг JSON может вызвать ошибку
const data = JSON.parse(await getJSON())
console.log(data)
} catch (err) {
console.log(err)
}
}
?3. Проверка условий и выполнение асинхронных действий
Представьте, что надо написать кусок кода, который, загрузив некие данные, принимает решение о том, вернуть ли их в точку вызова, или, основываясь на том, что уже получено, запросить ещё что-нибудь. Решить подобную задачу можно так:
const makeRequest = () => {
return getJSON()
.then(data => {
if (data.needsAnotherRequest) {
return makeAnotherRequest(data)
.then(moreData => {
console.log(moreData)
return moreData
})
} else {
console.log(data)
return data
}
})
}
Только от взгляда на эту конструкцию может разболеться голова. Очень легко потеряться во вложенных конструкциях (тут их 6 уровней), скобках, командах возврата, которые нужны лишь для того, чтобы доставить итоговый результат главному промису.
Код будет гораздо легче читать, если решить задачу с использованием async/await.
const makeRequest = async () => {
const data = await getJSON()
if (data.needsAnotherRequest) {
const moreData = await makeAnotherRequest(data);
console.log(moreData)
return moreData
} else {
console.log(data)
return data
}
}
?4. Промежуточные значения
Возможно, вы встречались с ситуацией, когда вы вызываете
promise1
, затем используете то, что он возвращает, для вызова promise2
, потом задействуете оба результата от ранее вызванных промисов для вызова promise3
. Вот как будет выглядеть код, решающий такую задачу.const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return promise2(value1)
.then(value2 => {
// do something
return promise3(value1, value2)
})
})
}
Если для
promise3
не нужно value1
, можно без особых сложностей упростить структуру программы, особенно если вам режут глаз подобные конструкции, использованные без необходимости. В такой ситуации можно обернуть value1
и value2
в вызов Promise.all
и избежать ненужных вложенных конструкций.const makeRequest = () => {
return promise1()
.then(value1 => {
// do something
return Promise.all([value1, promise2(value1)])
})
.then(([value1, value2]) => {
// do something
return promise3(value1, value2)
})
}
При таком подходе семантика приносится в жертву читабельности кода. Нет причин для того, чтобы помещать
value1
и value2
в один и тот же массив за исключением того, чтобы избежать вложенности промисов.То же самое можно написать с применением async/await, причём делается это удивительно просто, а то, что получается, оказывается интуитивно понятным. Тут поневоле задумаешься о том, сколько полезного можно сделать за то время, которое тратится на написание хоть сколько-нибудь приличного кода с использованием промисов.
const makeRequest = async () => {
const value1 = await promise1()
const value2 = await promise2(value1)
return promise3(value1, value2)
}
?5. Стеки ошибок
Представьте себе фрагмент кода, в котором имеется цепочка вызовов промисов, а где-то в этой цепочке выбрасывается ошибка.
const makeRequest = () => {
return callAPromise()
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => callAPromise())
.then(() => {
throw new Error("oops");
})
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
Стек ошибки, возвращённый из цепочки промисов, не содержит указания на то, где именно произошла ошибка. Более того, сообщение об ошибке способно направить усилия по поиску проблемы по по ложному пути. Единственное имя функции, которое содержится в сообщении —
callAPromise
, а эта функция никаких ошибок не вызывает (хотя, конечно, тут есть и полезная информация — сведения о файле, где произошла ошибка, и о номере строки).Если же взглянуть на подобную ситуацию при использовании async/await, стек ошибки укажет на ту функцию, в которой возникла проблема.
const makeRequest = async () => {
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
await callAPromise()
throw new Error("oops");
}
makeRequest()
.catch(err => {
console.log(err);
// вывод
// Error: oops at makeRequest (index.js:7:9)
})
Подобное нельзя назвать огромным плюсом, если речь идёт о разработке в локальном окружении, когда файл с кодом открыт в редакторе. Но это оказывается весьма полезным, если вы пытаетесь понять причину ошибки, анализируя лог-файл, полученный с продакшн-сервера. В подобных случаях знать, что ошибка произошла в
makeRequest
, лучше, чем знать, что источник ошибки — некий then
, вызванный после ещё одного then
, который следует за ещё каким-то then
…?6. Отладка
Этот пункт последний, но это не значит, что он не особо важен. Ценнейшая особенность использования async/await заключается в том, что код, в котором задействована эта конструкция, гораздо легче отлаживать. Отладка промисов всегда была кошмаром по двум причинам.
1. Нельзя установить точку останова в стрелочных функциях, которые возвращают результаты выполнения выражений (нет тела функции).

Попробуйте поставить где-нибудь в этом коде точку останова
2. Если вы установите точку останова внутри блока
.then
и воспользуетесь командой отладчика вроде «шаг с обходом» (step-over), отладчик не перейдёт к следующему .then
, так как он может «перешагивать» только через синхронные блоки кода.При использовании async/await особой нужды в стрелочных функциях нет, и можно «шагать» по вызовам, выполненным с ключевым словом
await
так же, как это делается при обычных синхронных вызовах.
Отладка при использовании async/await
Замечания
Вполне возможно, у вас возникнут некоторые соображения не в пользу применения async/await, продиктованные здоровым скептицизмом. Прокомментируем пару наиболее вероятных.
- Эта конструкция делает менее очевидными асинхронные вызовы. Да, это так, и тот, кто привык видеть асинхронность там, где есть коллбэк или
.then
, может потратить несколько недель на то, чтобы перестроиться на автоматическое восприятие инструкций async/await. Однако, например, в C# подобная функциональность есть уже многие годы, и те, кто с этим знакомы, знают, что польза от неё стоит временных неудобств при чтении кода.
- Node 7 — не LTS-релиз. Это так, но совсем скоро выйдет Node 8, и перевод кода на новую версию, вероятно, не потребует особых усилий.
Выводы
Пожалуй, async/await — это одна из самых полезных революционных возможностей, добавленных в JavaScript в последние несколько лет. Она даёт простые и удобные способы написания и отладки асинхронного кода. Полагаем, async/await пригодится всем, кому приходится писать такой код.
Уважаемые читатели! Как вы относитесь к async/await? Пользуетесь ли вы этой возможностью в своих проектах?
Комментарии (182)
dreammaster19
10.04.2017 15:50+12В конструкции then не надо оборачивать в новый промис c then, поскольку return возвращает промис, надо просто продолжить цепочку. async/await конечно кросиво и удобно но не дает какой-то панацеи и в некоторых случаях промисы очень удобны. Взять хотябы Promise.all(). Так что не понимаю зачем делать категоричные заявления об отказах от текущего функционала, если можно просто всё использовать в нужных мустах
Razaz
10.04.2017 16:25+1К слову в C# есть await Task.WhenAll(awaitable1, awaitable2). Как я понимаю если Promise.all() возвращает промис, То можно сделать то же самое?
Quilin
10.04.2017 16:42+6Определенно можно. Когда в C# появился синтаксис async/await, у меня лично весь асинхронный код стал гораздо симпатичнее и понятнее. Дичайше обрадовался тому же синтаксису в JS, они молодцы что выбрали именно C# в качестве примера для подражания.
dreammaster19
10.04.2017 17:46Я к тому, что нет await all, да и async это лишь синтаксический сахар для промисов, так что про них не стоит забывать. Но вот на счет исключений в async/await я полностью согласен с автором, гораздо удобнее
Pongo
10.04.2017 18:56+2async это лишь синтаксический сахар для промисов
Джаваскрипт по-особому отрабатывает await в циклах и условных операторах — с промисами такого поведения нет — поэтому это нечто большее, чем синтаксический сахар.
raveclassic
10.04.2017 23:31async это лишь синтаксический сахар для промисов
нечто большее, чем синтаксический сахар.
Я в кишках V8 не копался, но у меня есть стойкое подозрение, чтоasync/await
работает поверх генераторов с резолвом за-yield
-енного промиса при прогоне результирующего итератора. Результат.catch
же скармливается.throw
. Слишком уж «гибкий» для промисов флоу получается, на них такого без бубнов не сделать, взять те же циклы.
Посмотрите, во что бабель транспайлит async/await с включенным transform-async-to-generator. Ну очень похоже, что V8 делает то же самое, при чем на том же JS, как в случае с промисами.Sirion
11.04.2017 01:08Если я перечитал этот коммент два раза и всё равно не понял, мне ещё можно быть веб-девелопером или пора уходить в домохозяйки?
raveclassic
11.04.2017 10:05Постараюсь подробнее. Возьмем небольшой пример, сдобренный
async/await
:
//исходник с разными вариантами async/await const delay = ms => new Promise(resolve => { setTimeout(() => resolve(ms), ms); }); const throwAsync = (error, ms) => new Promise((resolve, reject) => { setTimeout(() => reject(new Error(error)), ms); }) const foo = async (n, shouldThrow) => { for (let i = 1; i < n + 1; i++) { const result = await delay(i * 1000); console.log('wait', result); } if (shouldThrow) { await throwAsync('Should throw', 1000); } return 'done'; } foo(3).then(::console.log).catch(::console.error); foo(3, true).then(::console.log).catch(::console.error);
//результат после бабеля с transform-async-to-generator var _context; function _asyncToGenerator(fn) { return function() { var gen = fn.apply(this, arguments); return new Promise(function(resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function(value) { step("next", value); }, function(err) { step("throw", err); }); } } return step("next"); }); }; } const delay = ms => new Promise(resolve => { setTimeout(() => resolve(ms), ms); }); const throwAsync = (error, ms) => new Promise((resolve, reject) => { setTimeout(() => reject(new Error(error)), ms); }); const foo = (() => { var _ref = _asyncToGenerator(function*(n, shouldThrow) { for (let i = 1; i < n + 1; i++) { const result = yield delay(i * 1000); console.log('wait', result); } if (shouldThrow) { yield throwAsync('Should throw', 1000); } return 'done'; }); return function foo(_x, _x2) { return _ref.apply(this, arguments); }; })(); foo(3).then((_context = console).log.bind(_context)).catch((_context = console).error.bind(_context)); foo(3, true).then((_context = console).log.bind(_context)).catch((_context = console).error.bind(_context));
Уделите внимание функции_asyncToGenerator
, как она работает с промисами, принимаемыми изstep("next")
. Еще посмотрите, как всеawait
заменились наyield
абсолютно в тех же местах без перекомпоновки кода, что пришлось бы делать при переписывании на промисы.
Исходный посыл был к тому, что есть очень большие подозрения, что async/await внутри V8 сделан не на основе промисов, а как раз на основе генераторов.
Поиграться можно вот с таким набором пресетов.mayorovp
11.04.2017 10:12Исходный посыл был к тому, что есть очень большие подозрения, что async/await внутри V8 сделан не на основе промисов, а как раз на основе генераторов.
Почему вы разделяете эти два случая?
raveclassic
11.04.2017 10:22Не разделяю, а дополняю следующее:
async это лишь синтаксический сахар для промисов
Действительно, промисы и генераторы работают в тандеме в случае async/await. Async функция — это возвращаемый промис, а await точки — yield в генераторе.
raveclassic
11.04.2017 10:24Отсюда и древний холивар, что async/await не нужен, так как все то же самое можно сделать на генераторах и маленькой функции-хелпере.
iShatokhin
11.04.2017 15:26Да, но зато можно определить через символы является ли функция async:
var fn = async function () {}; fn[Symbol.toStringTag] === "AsyncFunction";
bano-notit
11.04.2017 16:41Ну и в чём прикол? В том что они реализованы на уровне движка js? Какая разница, они все реализованы одинаково по смыслу, просто так они реализованы в движке, а так их нужно реализовывать самим.
raveclassic
11.04.2017 17:12И зачем вам это, скажите, пожалуйста?
Через символы можно много чего определить, в том числе и генераторы:
(function * foo() {})[Symbol.toStringTag] === 'GeneratorFunction'
Другое дело, что смысловой нагрузки это никакой не несет.iShatokhin
11.04.2017 17:13Пример использования — https://github.com/caolan/async/pull/1390
raveclassic
11.04.2017 17:30Это все, конечно, прекрасно, только из вашего примера вытекает только то, что
async/await
существует для того, чтобы его можно было задетектить черезSymbol.toStringTag
. Генераторы тоже детектятся, а решают те же задачи и даже чуть больше.
И если уж вы используете стороннюю библиотеку, то хелпер для «размотки» генератора есть в co.iShatokhin
11.04.2017 17:35Я не пользуюсь именно этой библиотекой, просто пример, где это используется. Сам я буду использовать "AsyncFunction" немного по другому, для определения функций в обертке и отслеживании вывозов API внутри системы (аналитика производительности, мониторинг ошибок).
mayorovp
11.04.2017 08:34Нет никакого противоречия. Когда метод в конечный автомат или генератор превращает транслятор — это нормально. Когда то же самое делает программист вручную — это пляски с бубном и костылями.
raveclassic
11.04.2017 10:02Нет никакого противоречия.
Хм, я не имел в виду противоречие про сахар, а просто объединил =)
unel
11.04.2017 22:37Я бы не стал утверждать, что любой конечный автомат, реализованный программистом — это пляски с бубном и костылями.
Если это требуется для реализации задачи — то почему нет?
DistortNeo
11.04.2017 14:43Не знаю, как в JS, а в C# за асинхронными функциями стоит довольно суровая кодогенерация. Грубо говоря, каждая функция разворачивается в класс, переменные внутри функции — в поля класса, а ветки выполнения, содержащие await — либо в отдельные функции, либо в функцию с большим switch (можно и так, и так реализовывать машину состояний).
Можно этот класс написать вручную, тогда код с использованием async/await будет идентичен промисам.
Так что async/await — это именно сахар.raveclassic
11.04.2017 14:47async/await раскладывается бабелем при транспайлинге в es5 как раз в свитч вместе с regeneratorRuntime (рантайм который этот свитч перемалывает). Собственно, генераторы раскладываются в похожий свитч с тем же рантаймом.
Quilin
10.04.2017 19:19К сожалению, не очень хорошо разбираюсь в подкапотной работе V8 в оптимизации async/await, но немножко могу про C#, и там компилятор очень умеет оптимизировать код. Скажем, когда результаты функций должны использоваться совместно:
let a = await _a() let b = await _b() let c = await _c(a, b) console.log(c)
В мире с промисами это будет выглядеть как-то вот так:
Promise.all(_a(), _b()) .then(r => _c(r[0], r[1])) .then(console.log)
Может быть и ничего так, но гораздо сложнее для чтения и понимания, а если между строками ставить еще какие-нибудь логи или вызовы синхронных функций, то код на промисах станет гораздо сложнее.
В C# эти страшные «промисы» (Task) резолвит внутренний оптимизатор, по сути оно во что-то такое и превращается, но пользоваться этим гораздо удобнее, имхо.Apathetic
10.04.2017 20:45Нет. Ваш пример с await с обычными промисами выглядел бы так:
_a() .then(a => _b().then(b => _c(a, b)) .then(console.log);
То есть еще хуже, чем вы написали. Сначала выполнится промис _a, и только потом — _b.
Чтобы промисы работали синхронно, действительно нужно использовать Promise.all:
let [a, b] = await Promise.all([_a(), _b()]); let c = await _c(a, b); console.log(c);
mayorovp
10.04.2017 21:39-1Необязательно. Можно же и вот так сделать:
let a = _a(); let b = _b(); let c = _c(await a, await b); console.log(await c);
Но с тем, что первый пример был некорректен — согласен.
Apathetic
10.04.2017 21:56Нельзя. Сделаем такой простенький промис для демонстрации:
const p = (a, b) => new Promise(resolve => setTimeout(resolve, 1000, b ? { a, b } : a));
Вопрос на засыпку: когда в консоли появится результат — через секунду, две или три?
(async () => { console.log(await p(await p(1), await p(2))); })()
Или более развернуто (как у вас):
(async () => { let a = p(1); let b = p(2); let c = p(await a, await b); console.log(await c); })()
mayorovp
10.04.2017 21:58-1Вы изменили мой код! Он должен выглядеть вот так:
(async () => { let a = p(1); let b = p(2); console.log(await p(await a, await b)); })()
Результат в консоли появится через 2 секунды. А приведенный вами код покажет его через 3 секунды.
Apathetic
10.04.2017 22:10Вообще, развернутый вариант у меня в комментарии тоже за 2 секунды отработает.
Причины такого поведения понятны, но совершенно (ИМХО) не очевидны. Попробую использовать вопрос о разнице между этими двумя кусками кода и её причинах на собеседованиях =)
Apathetic
10.04.2017 22:13+1То есть получается, там, где я сказал "нельзя", на самом деле можно. Довольно-таки глупо с моей стороны не заметить, что оба промиса (a и b) запускаются сразу друг за другом.
HaMI
10.04.2017 16:11-5Всю статью можно свести к этому предложению
>Однако, например, в C# подобная функциональность есть уже многие годы, и те, кто с этим знакомы, знают, что польза от неё стоит временных неудобств при чтении кода.
Я не луддит мне просто не очевидно зачем менять один синтаксис на другой.
Второе, все приведенные «плюшки» совсем меркнут если использовать bluebird – он очень улучаешт работу с ошибками и дает кучу утилит для координации промисов.
HaMI
10.04.2017 17:29-1стоп, кто говорил про callback спагетти? предлагают забыть о промисах
тут предлагают использовать async/await и забыть о промисах делая чуть короче простые примеры. Но ничего не сказано про более сложные кейсы – что делать с типизацией ошибок? как координировать несколько промисов? все это решает bluebird
второе, статья манипулятивна:
const makeRequest = () => { return promise1() .then(value1 => { // do something return Promise.all([value1, promise2(value1)]) }) .then(([value1, value2]) => { // do something return promise3(value1, value2) }) }
так выглядит намного короче
const makeRequest = () => promise1() .then(value1 => Promise.all([value1, promise2(value1)])) .then(([value1, value2]) => promise3(value1, value2))
а с блюбердом совсем так:
const makeRequest = () => promise1() .then(value1 => [value1, promise2(value1)]) .spread((value1, value2) => promise3(value1, value2))
>Движение языка в сторону упрощения
js и так простой, то что с этой «фичей» ассинхронный код начинает выгдлядеть как синхронных – сомнительный плюс.
код ниже не имеет смысла – с тем же успехом можно писать синхроно и не заморачиваться и есть вероятность что даже быстрее будет
const makeRequest = () => { return callAPromise() .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise()) .then(() => callAPromise())
Nakosika
10.04.2017 17:37+2Ну в статье довольно понятно какие плюсы. Из каллбеков к примеру ретурн не сделать или трайкатч, потому что вся информация о стеке уже будет потеряна.
Nakosika
10.04.2017 17:43Я сам по началу не разобрался и думал нафига козе баян, а код не просто выглядит как синхронный, он им и является, потому что стек сохраняется отсюда все плюсы.
HaMI
10.04.2017 17:54у меня сложилось впечатление что вы путаете callback и promise.
>он им и является, потому что стек сохраняется отсюда все плюсы.
вы уверены в своих словах? в чем профит? юзайте все что *sync из стандартной библиотеки – будет вам счастье.Nakosika
10.04.2017 18:09В пропозишен вроде так написано, но сам я еще палкой не тыкал туда. Надо будет проверить что у них вышло.
spmbt
10.04.2017 17:47Да, коллбеки (промисы) с исключениями — самая большая у них проблема. Раньше просто async не было, поэтому предлагали меньшее зло, тем более, что того же синтаксиса, но упорно шли к async и чуть раньше — к урожаю: ).
Deosis
11.04.2017 07:55+2Вот только вы не учли, что под // do something может оказаться страница кода, которую вы смело выкинули.
HaMI
11.04.2017 11:39ага, вам не показалось странным что в примере с Promise «do something» есть, а в примере с async/await нет?
или вы думаете что «do something» во втором случае будет меньше?
HaMI
10.04.2017 18:02+1Если располагать тоже количество «логики» на строчку кода, то выйдет что примеры аналогичны по длине
Например:
const makeRequest = async () => { console.log(await getJSON()) return "done" } //против: const makeRequest = () => getJSON().then(console.log) .then(()=> 'done')
Claud
10.04.2017 18:35async () => { let t1 = await call(); let t2 = await call(); let t3 = await call(); }
Я верно понимаю, что в примере выше call() будет вызываться асинхронно, и не дожидаться завершения предыдущих вызовов?mayorovp
10.04.2017 19:00+2Нет. Каждый await ожидает завершения прежде чем передать управление дальше.
Чтобы было как вы написали — надо делать так (пишу по памяти, могу напутать с синтаксисом немного):
var [f1, f2, f3] = await Promise.all([call(), call(), call()])
HaMI
10.04.2017 19:38тут важно добавить – что call таки может быть «асинхронным»(например вызвать что-то по сети) и не будет блокировать event-loop(а значит где-то в другом месте что-то может выполнится)
Но сам кусок написан так что call'ы будут выполнятся по очень – если нужно условно одновременно, то нужен Promis.all как mayorvp написал
Maxxon
18.04.2017 01:22как вариант без Promise.all
async () => { let t1 = call(); let t2 = call(); let t3 = call(); let f1 = await t1; let f2 = await t2; let f3 = await t3; }
HaMI
18.04.2017 12:42Что вы хотели показать своим примером?
Maxxon
18.04.2017 13:17Очевидно, что то что и написал, код выше эквивалентен этому
var [f1, f2, f3] = await Promise.all([call(), call(), call()])
iShatokhin
18.04.2017 19:12+1Не эквивалентен, т.к. try-catch с таким кодом не работает, реально будет поймано исключение только для первого await, остальные уйдут в "Unhandled promise rejection" (с Promise.all такой проблемы не будет). В соседнем топике обсуждалось — https://habrahabr.ru/post/326442/#comment_10177554
Maxxon
18.04.2017 21:30Ну так вроде ничто не мешает каждый await обернуть в try-catch, за одно удобнее различные исключения бросать
bano-notit
18.04.2017 17:46Последовательно, опять же… А нужно именно паралельное. Допустим t1 будет идти 7 мс, t2 — 3 мс, t3 — 12 мс. Если сделать через
Promise.all()
, то даже если мы будем учитывать, что на вызов call уходит само по себе 1 мс, получится всего max(12, 7, 3) + 3 = 15 мс, в отличии от ваших последовательных 7 + 1 + 3 + 1 + 12 + 1 = 25 мс. Мне кажется, что преимуществоPromise.all()
достаточно очевидно.HaMI
18.04.2017 18:15+1bano-notit
вы не поверите, но таки пресдтавленный maxxon код «почти»(с оговорками для ноды) паралельный и медленнее того который с Promise.all на ~2ms – и то, есть вероятность, что это лаг, а не реальное замедление
Другой вопрос – что это не очевидно, и синкаксически его код не показывает что он «паралельнный»bano-notit
18.04.2017 18:23Блин… Согласен, не досмотрел. Ведь промисы все просто в переменных сохраняются, а не генерятся на самом await...
Но то, что это совершенно не очевидно — 100%.
TheShock
18.04.2017 18:25+2Другой вопрос – что это не очевидно, и синкаксически его код не показывает что он «паралельнный»
call()
— запуск запроса, а await — ожидания его выполнения. То есть сначала запускается три запроса и только потом запускается их ожидание. Мне было вполне очевидно, что они исполняются параллельно. Видимо, необходимо базово понимать, как этот код работает.
mayorovp
19.04.2017 07:48Ну, тут проблема не столько в оператре await, сколько в функции call. По названию не видно что она асинхронная.
comerc
10.04.2017 20:58-3В статье ни слова про это: https://habrahabr.ru/post/320306/
TheShock
10.04.2017 21:41+4А зачем впоминать про статью демагога, которая может вызвать только фейспалм?
bano-notit
11.04.2017 00:39Вопрос такой: как с помощью async/await организовать тот же самый
Promise.all()
? Ведь в таком коде
let a = await first(), b = await second();
Второй промис будет выполнен только после того, как выполнится первый. А ведь второй от первого не зависит, так что такой вот код сделать нормальным можно только вот так вот:
let [a, b] = await Promise.all([first(), second()]).then((a, b)=> [a, b]);
А это как-то не сильно входит в идею синтаксического сахорочка...
bano-notit
11.04.2017 00:45-1Как показали тесты
.then()
в данном случае не является нужным… Но всё равно конструкция получается не сильно сладкая.
Akuma
11.04.2017 00:52+1Вот тут ответили:
https://habrahabr.ru/company/ruvds/blog/326074/#comment_10164346
Но, честно говоря, с Promise.all как-то интуитивней становится. А то слишком замороченно получается.bano-notit
11.04.2017 00:54Да вообще-то можно было бы вообще для всего промис интерфейса сделать сахарок, например логические операторы для промисов) Это было бы вообще офигительно нечитаемо.
TheShock
11.04.2017 02:31ну не знаю, по-моему вполне читаемо:
let [a, b] = await [ first(), second() ];
bano-notit
11.04.2017 02:36Не согласен. потому что по идее эта конструкция должна выглядеть так:
let [a, b] = [await first(), await second()];
Ведь массив не является промисом… А в качестве синтаксического сахарка принимать массивы…
Ну это получается нужно ещё и такую вещь реализовывать:
let {first: a, second: b} = await {first: first(), second: second()}
Да и что тогда делать, например с такой вещью?
let [a, b] = await [1, second()]
Ведь такое вполне может произойти…
Promise.all()
с этим справляется, но вот реализовать такое на синтаксическом сахаре будет уже много менее явно и понятно со стороны.TheShock
11.04.2017 02:39let {first: a, second: b} = await {first: first(), second: second()}
Это валидно, остальное пусть валится с ошибкой.bano-notit
11.04.2017 02:41Хорошо так придумано! То есть
Promise.all()
может, а синтаксический сахар к нему пусть с ошибкой валится...TheShock
11.04.2017 02:45Да, тут вы правы. Интерфейс стоит как у all сделать.
bano-notit
11.04.2017 02:48Тогда без объектов… Тогда не весь изначальный синтаксический сахар присвоения значений получается… Короче говоря дилемма.
TheShock
11.04.2017 02:58JS — вечная проблема с тем, что новые фичи спотыкаются о старые костыли. Я б возможности .all добавил, но не убирал бы логически ожидаемое поведение с объектами.
bano-notit
11.04.2017 03:04Понимаете ли, в этом то и прикол, что нужно либо добавлять объекты внутрь
Promise.all()
, что не сильно влечёт за собой проблемы, либо не делать этой прикольной штуки для объектов, либо сделать ещё умнее, сделатьawait
отдельным оператором, который принимает только промисы, а остальные значения возвращает черезPromise.reslove()
, как собственно и сделали они)TheShock
11.04.2017 03:17-1Не могу согласиться, что умнее. Промисы — костыль своего времени из-за отсутствия евейта. Теперь мы продолжаем ставить костыли, чтобы соответствовать старым костылям и из-за этого недостаточно развиваемся.
Да, должна быть обратная совместимость, т.е. код, который раньше использовал промисы легко переключается на эвейт, но это не значит, что мы должны слепо следовать устаревшим контрактам.bano-notit
11.04.2017 03:31На счёт поддержки старого у меня почему-то вспоминается сразу ie… Не знаю почему, но ассоциация у меня прям жёсткая, когда слышу/читаю слово "совместимость"…
А на счёт того, что должны или нет, тк вопрос в том, что js сам по себе начинает поддерживаться по кускам, поэтому сейчас есть смысл вводить только сахарок, который можно будет переделать на старые версии. Тут это логично, потому что есть до сих пор люди, которые сидят на ie7-9, которым про наши холивары "что лучше, Promise или await/async" вообще пофигу, у них не работает ни то, ни другое по дефалту.TheShock
11.04.2017 03:35У них async-await и так и так не работает, потому можно сделать нормальный интерфейс.
bano-notit
11.04.2017 03:49Я именно про этом, потому что там нужно, получается, 2 раза фигачить: 1 — async/await трансформировать в промисы; 2 — взять и подключить отдельную либу для того, чтобы хотя бы не нативыне, но промисы были.
mayorovp
11.04.2017 08:39+1Промисы — костыль своего времени из-за отсутствия евейта.
Нет, это не так. Невозможно сразу сделать оператор await, не вводя в язык промисы или их аналоги.
bano-notit
11.04.2017 03:06На счёт
as
они просто попытались спереть у питона… Но у них не получилось, потому что модули в js всё равно редатировать труднее, чем питоновские, там потому что имя модуля, из которого всё берётся находится раньше вещей, которые из него берутся, что в insert mode ещё хоть как-то можно понять. А вот когда тебе кроме того, что изменить импорты нужно ещё и путь к модулю запоминать, вот тут уже начинается портативный ад.TheShock
11.04.2017 03:21Лучше бы они раньше взяли бы пример с питона и сделали нормальную деструктуризацию. А импорты да — крайне кривые.
unel
11.04.2017 13:11+1Глядя на некоторые из этих примеров использования промисов и правда может разболеться голова, ведь промисы как раз таки и были задуманы для решения callback-hell и этого адского уровня вложенности.
Но некоторые из них же можно переписать!
например, вместо
этого кода:const makeRequest = () => { return getJSON() .then(data => { if (data.needsAnotherRequest) { return makeAnotherRequest(data) .then(moreData => { console.log(moreData) return moreData }) } else { console.log(data) return data } }) }
Shannon
11.04.2017 15:48Можно-то можно, даже callback-hell можно переписать и превратить во что-то приятное и понятное
Но вот такое:
async function gogogo(queryList) { let resultJson = {} for (const query of queryList) { let json = {} const type = await getType(query) if (type === 1) { json = await getJson(query) } else if (type === 2) { const rawData = await getRawData(query) json = await rawToJson(rawData) } else { break } Object.assign(resultJson, json) } return resultJson } async function doSomething() { const resultJson = await gogogo(queryList) const html = templateGenerator(resultJson) console.log(hmtl) } doSomething()
Уже становится немного сложновато представить на промисах, особенно если какой-то еще логики надо добавитьHaMI
11.04.2017 17:42Cорри, по понятным причинам не тестал. Попрошу обратить еще внимание на http://bluebirdjs.com/docs/api/promise.map.html#map-option-concurrency – что-то мне подсказывает что оно еще и работать будет быстрее так постарается вызвать все запросы пораньше(как работает async/await в случае цыкла – не уверен, но что-то мне кажется что наш код станет псеводо-синхроным для этого цыкла)
const Promise = require('bluebird') function gogogo(queryList) { return Promise .resolve(queryList) .map(getType) // can be incapsulated in a function but original sample doesn't do that .map(function(type) { if (type === 1) { return getJson(query) } else if (type === 2) { return getRawData(query) .rawToJson(rawData) } else { return {} } }) .reduce(Object.assign, {}) } function doSomething() { gogogo(queryList) .then(templateGenerator) .tap(console.log) } doSomething()
HaMI
11.04.2017 17:51пока писал это понял – промисы нравятся тем кто предпочитает .map, .filter, .reduce циклам и наоборот async/await – любителям циклов
Хорошо, это или плохо – не знаю, это тема для извечного холивара. Но мне все же кажется, что async/await – позволяет писать в псевдо-синхронной манере игнорируя понимаю когда произойдет асинхронная операция
ну и заодно интересно – есть ли щанс достичь аналогичного поведения с async/await? с concurrency = Infinity или, например, concurrency = 10raveclassic
11.04.2017 18:03Вот вам классическая задача.
Есть у вас массив урлов. Запрос по каждому из них возвращает ключ, и каждый, начиная со второго, принимает в запросе ключ, полученный от предыдущего запроса. Количество урлов не известно. Нужно получить последний ключ. И давайте договоримся, всевозможные хелперы из всяких bluebird и компании использовать нельзя.raveclassic
11.04.2017 18:07Ради интереса, представим, что у нас еще очень маленький стэк, а урлов может быть больше 10к.
HaMI
11.04.2017 18:08давайте договоримся что вы будете использовать – Pascal или js из 7 осла? Неиспользовать технологии – это луддизм
Кроме того, начните сначала с себя. Ваш комментарий без кода выглядит, мягко говоря, голословноraveclassic
11.04.2017 18:20+2Неиспользовать технологии – это луддизм
Мы как раз обсуждаем технологию: async/await или промисы. Не надо тащить сюда ворох библиотек, облегчающих работу с промисами и инкапсулирующих бойлерплейт.
Вот вам пример.
declare const get: (url: string, key: string) => Promise<string>; const getKey = async (initial: string, urls: string[]): Promise<string> => { let result = initial; for (const url of urls) { result = await get(url, result); } return result; };
PS. простите мне мой TSHaMI
11.04.2017 18:51>Не надо тащить сюда ворох библиотек
одну и странно слышать это от человека предоставивщего пример на typescript
const Promise = require('bluebird') const getKey = (initial, urls) => Promise.resolve(urls) .reduce( key, url => get(url, key), initial )
raveclassic
11.04.2017 18:56Пожалуйста, вот вам обычный ES:
const getKey = async (initial, urls) => { let result = initial; for (const url of urls) { result = await get(url, result); } return result; };
Что вы везде этот bluebird тащите? Можете руками написать асинхронный редьюс?HaMI
11.04.2017 19:01+1>Что вы везде этот bluebird тащите?
очевидно – нормальная обработка ошибок и приятные фичи
>Можете руками написать асинхронный редьюс?
думаю смогу, но давайте не будем это проверять. Потому что я попрошую взамен гарантий что вы сможете руками добавить в язык async/awaitraveclassic
11.04.2017 19:03+1Кроме того, начните сначала с себя.
думаю смогу, но давайте не будем это проверять.
Вы что-то мечетесь, неужели так сложно без bluebird?
Потому что я попрошую взамен гарантий что вы сможете руками добавить в язык async/await
Они уже в языке и спеке.mayorovp
11.04.2017 19:08reduce тоже есть в языке и в спеке.
raveclassic
11.04.2017 19:12Асинхронный?
mayorovp
11.04.2017 19:16Если вам позарез нужен асинхронный — держите:
function reduceAsync(array, fn, initial) { return array.reduce( (prev, current, index) => prev.then(prev2 => fn(prev2, current, index, array)), Promise.resolve(initial) ); }
Но я всегда простым обходился.
HaMI
11.04.2017 19:24Так что по рукам или слились? вы берете рантайм от 51-го Firefox и допиливаете туда async/await
А если серьезно, давайте вы мне не будете расказывать – что я должен, а что нет.
>неужели так сложно без bluebird?
ага, а еще без underscore, jquery и еще вагона библиотек, которые в той или иной мере вошли в язык, Browser API
На этом – давайте закончим дискуссию, а то она несколько в другую плоскость перерешла.raveclassic
11.04.2017 19:31-1Так что по рукам или слились? вы берете рантайм от 51-го Firefox и допиливаете туда async/await
Это вы слились. Вы тащите стороннее апи для расширения существующей спецификации только для того, чтобы упорно пытаться решить задачу, для которой в язык введен отдельный инструмент, не используя этот инструмент. Главное преимущество async/await и генераторов — возможность удобно работать с циклами. Вы так здорово приводите примеры на промисах в ситуациях, когда с их помощью действительно можно решить поставленную задачу, а примера с циклом я от вас так и не дождался.
Ну что ж, действительно, закончим.
TheShock
11.04.2017 20:07очевидно – нормальная обработка ошибок и приятные фичи
То есть, встроенные в язык Промисы по вашему — не юзабельны?
unel
11.04.2017 19:00-2ну вот без await
const getKey = (prevKey, remainingUrls) => { if (!remainingUrls.length) { return Promise.resolve(prevKey); } else { return getKeyFromApi(remainingUrls[0]).then(newKey => { return getKey(newKey, remainingUrls.slice(1)) }); } }
raveclassic
11.04.2017 19:04Я выше написал про маленький стэк. Можете без рекурсии?
unel
11.04.2017 19:20+1насколько я помню, в случае асинхронных операций, стек не съедается таким образом (я даже проверил это для 18000 урлов с текущей глубиной стека в 17800)…
где-то даже видел "хак" в виде tail-call optimisation, когда себя же вызывают через setTimeout)
вот по памяти, я думаю, будет не очень оптимально, это да.
mayorovp
11.04.2017 19:27Чтобы было оптимально по памяти — надо вместо slice использовать растущий индекс. Получится даже оптимальнее чем через reduce.
raveclassic
11.04.2017 19:35Я вам тут отвечу.
Спецификация промизов явным образом требует от рантайма чтобы колбек, переданный в then, запускался на пустом стеке (формально — не содержащем никаких пользовательских фреймов).
Про спеку и пустой стэк не знал, спасибо. Я не особо силен в этой внутрянке, но разве аргументы функции не образуют новый скоуп, который где-то должен лежать, даже при очистке стэка? Это уже к вопросу расхода памяти.mayorovp
11.04.2017 20:55Цепочка хранимых скоупов не может быть длиннее чем вложенность кода в редакторе и не зависит от глубины вызовов.
И таких цепочек хранится всего одна — для следующей итерации.
raveclassic
11.04.2017 21:37Цепочка хранимых скоупов не может быть длиннее чем вложенность кода в редакторе и не зависит от глубины вызовов
Стоп, как это не зависит от глубины? Вам в рекурсивную функцию в каждом вызове приходят новые значения, они должны храниться.mayorovp
13.04.2017 17:29Но это каждый раз будет новая цепочка, с той же самой глубиной.
raveclassic
13.04.2017 17:35Ну так дело в то не в глубине, а в их количестве. На каждый виток рекурсии мы выделяем память заново под новые аргументы. И ради чего, чтобы не использовать await?
mayorovp
13.04.2017 17:44Вы так говорите как будто await ничего не выделяет при вызове :-)
raveclassic
13.04.2017 18:05Выделяет, но цикл — не рекурсия.
mayorovp
13.04.2017 18:09Опять двадцать пять… Почему вы считаете рекурсию безусловно хуже цикла, если она не требует больше памяти и не ест стек?
raveclassic
13.04.2017 18:52+1Ну где она не есть память-то?
Но это каждый раз будет новая цепочка,
Старая куда денется? Уничтожится? Зачем наматывать эти цепочки рекурсивно, когда можно просто пройтись в рамках одного скоупа циклом по массиву, сохраняя ссылку на ключ?
Сравните 2 примера выше — в рекурсивном варианте вы выделяете память каждый раз заново для всей начинки getKey при вызове. Слайс массива — это вообще надругательство.mayorovp
13.04.2017 19:26Про слайс массива я уже тоже писал, его надо на переменную с индексом заменить или на использование итератора.
Прошу обратить внимание, что все существующие js-реализации async/await используют рекурсивный вызов на каждый оператор await. И нет оснований предполагать что рантайм браузера будет реализован сильно оптимальнее.
raveclassic
13.04.2017 19:39Await основан на генераторах, которые реализованы нативно в самом движке. Так что там с памятью все в порядке (надеюсь).
HaMI
11.04.2017 19:04оно рекурсивное – вам сейчас расскажут про то что urlов больше чем размер стека. Вы можете это пофиксить извернувшись с .catch – но будет выглядеть уродски
mayorovp
11.04.2017 19:15Нет, эта функция — не рекурсивная и прекрасно работает на ограниченном стеке.
Спецификация промизов явным образом требует от рантайма чтобы колбек, переданный в then, запускался на пустом стеке (формально — не содержащем никаких пользовательских фреймов).
Shannon
11.04.2017 18:18О том и речь, понадобился функционал сторонней библиотеки
А вот это я с трудом представляю как изобразить, чтобы такой вариант заработал:
return getRawData(query) .rawToJson(rawData)
При то, что и getRawData и rawToJson асинхронные функции. Либо тут надо быть гуру промисов, чтобы в этом разобраться
Но даже при этом всё равно получилось не то же самое, так как «return {}» не тоже самое что «break», и в случае с map перебор продолжится, а в моей случае предпологался выход из циклаraveclassic
11.04.2017 18:21Либо тут надо быть гуру промисов, чтобы в этом разобраться
Нет, это просто опечатка, должно быть
getRawData(query).then(rawToJson)
HaMI
11.04.2017 18:40да, очепятка – такое бывает с кодом который нельзя протестировать
return getRawData(query) .then(rawToJson)
>Но даже при этом всё равно получилось не то же самое, так как «return {}» не тоже самое что «break», и в случае с map перебор продолжится, а в моей случае предпологался выход из цикла
как-то это не логично чуть-чуть(точно не continue)
но ок:
const Promise = require('bluebird') function gogogo(queryList) { return Promise .resolve(queryList) .map(getType) .then((types) => types.find(x => x !=== 1 && x !== 2) ) .map(function(type) { // can be incapsulated in a function but original sample doesn't do that if (type === 1) { return getJson(query) } else { return getRawData(query) .rawToJson(rawData) } }) .reduce(Object.assign, {}) } function doSomething() { gogogo(queryList) .then(templateGenerator) .tap(console.log) } doSomething()
>функционал сторонней библиотеки
мы, вроде бы, обсуждаем фичу которая почти везде возможна только с транспилером. Давайте, если на то пошло откажемся от всех библиотек сразу тогда. Представьте как будет интересно – все руками. Заодно и денег будем больше рубить с любого проекта.
Будет как 10 лет назад: «Ищу php-разработчика со своим фреймворком – любителей писать с нуля, просим не беспокоить»raveclassic
11.04.2017 18:50+1Давайте, если на то пошло откажемся от всех библиотек сразу тогда. Представьте как будет интересно – все руками.
И смешались в кучу кони люди. Не путайте часть спецификации языка и сторонние библиотеки.
Async/await не требует никаких библиотек для работы. Генераторы почти не требуют никаких библиотек для работы. Почти, потому-что можно раннер либо самому написать, либо взять из бабеля (в комментах выше), либо из co.
мы, вроде бы, обсуждаем фичу которая почти везде возможна только с транспилером.
Раз, два, три.HaMI
11.04.2017 18:56Мы же помним что это js? – тут все сначала стороняя библиотека, а потом идет в язык
Shannon
11.04.2017 18:52+1мы, вроде бы, обсуждаем фичу которая почти везде возможна только с транспилером
Нее, мы обсуждаем фичу которая нативно работает в node, и во всех актуальных браузерах, кроме Edge и Opera Mini
http://caniuse.com/#feat=async-functions
как-то это не логично чуть-чуть(точно не continue)
но ок:
Ну вот, про это и речь, «Уже становится немного сложновато представить на промисах, особенно если какой-то еще логики надо добавить»
Всё становится очень хрупким и нужно всю цепочку держать в голове, чтобы небольшие изменения внести. При этом никто не говорит, что это не возможно, возможно-то возможноHaMI
11.04.2017 19:46я вам привел как это будет выглядеть, сложности тут особой нет.
Дальше – рассуждайте сами, какой подход вам более покажется преемлемым
вопрос исключительно в том что стоит показывать равносильные примеры, а не те в которых один подход явно выгодней другого, но это не к вам вопрос – а к статьеShannon
11.04.2017 20:57+1Я про то и говорю:
Всё становится очень хрупким и нужно всю цепочку держать в голове, чтобы небольшие изменения внести. При этом никто не говорит, что это не возможно, возможно-то возможно
Достаточно чуть усложнить условие (приложение в процессе динамического развития, много экспериментов):
const type = await getType(query) const subtype = await getSubType(query) if (type === 1 && subtype === 4) { json = await getJson(query) } else if (type === 2 && !subtype) { const rawData = await getRawData(query) json = await rawToJson(rawData) } else if (subtype === 1) { break }
И всё становится чуточку сложнее и нужно поломать голову, как это впихнуть в промисы без промисс-хелла.
И я уверен, что это получится сделать, но я не уверен, что можно это сделать за 10 секунд, как это делается на async-await
Но забыть про промисы нельзя хотя бы потому, что с помощью промисов удобно оборачивать функции на коллбэках в асинхронные функции.
mayorovp
11.04.2017 21:16+1Вот зачем вы пишите
Promise.resolve(queryList).map(getType)
— когда можно написатьqueryList.map(getType)
? И будет работать без сторонних библиотек.
Вы же сначала сами переусложняете код — а потом жалуетесь что вас не понимают.
PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!
function gogogo(queryList) { const items = queryList.map(query => { const type = await getType(query); if (type === 1) { return getJson(query) } else if (type === 2) { const rawData = await getRawData(query) return rawToJson(rawData) } }) let resultJson = {} for (const item of await Promise.all(items)) { if (item === undefined) break; Object.assign(resultJson, item) } return resultJson; }
И да, я знаю что этот код не полностью эквивалентен исходному.
raveclassic
11.04.2017 21:33Вот оно! Есть правда некоторые неточности с отсутствующими кое-где async и await, но посыл-то верный, каждый инструмент ввели в язык для своих целей и использовать нужно все.
mayorovp
11.04.2017 21:44Нет, await у меня все на месте! А вот про async я и правда позабыл напрочь...
raveclassic
11.04.2017 21:51getJson же асинхронный? Мм, ну по логике, из названия. Хотя по изначальному коду и не скажешь.
Edit: Ааа, items это же промисы :)
HaMI
11.04.2017 23:12-1>Вот зачем вы пишите Promise.resolve(queryList).map(getType) — когда можно написать queryList.map(getType)?
Эти два куска кода совсем не эквиваленты. Посыл был показать concurrent и что выполнить n-запросов один за другим – это далеко не тоже самое что выполнить n-запросов «сразу». И что это важно – понимать эту разницу. Но это никто в упор не хочет видеть.
проблема вашего и моего кода в другом – мы делаем лишние вызовы к getType.
>PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!
Отличная мысль! давайте еще раз глянем в заглавие статьи. А потом посмотрим на примеры из нее – я не вижу там того что показал Shannon, но не смог сформулировать
А нужно было показать что в случаях с циклами с пост-условием(вот этот break) нам, таки, намного легче писать с async/await – и пофиг что это некрасиво, не-functional и вообще почти GOTO. И, наверное есть еще подобные примеры
В статье и комментариях я вижу как раз то о чем вы написали:
Promise.resolve([1, 2, 3]).map(getType)
сложнее чем(и впридачу, еквивалентно)
[1, 2, 3].map(query => { const type = await getType(query); return type })
только в действительности первый вариант будет выполнятcя ~n времени(конечно, с оговорками), а второй 3*n времени. И если эту разницу игнорировать – то нам вообще не нужен async/await(как и promise), нам просто нужно синхронное IO
Если я что-то не понимаю – прошу поправитьraveclassic
11.04.2017 23:57Вы придираетесь к разнице последовательного и параллельного выполнения. Да, разница видна, конечно же. Оба варианта можно сделать на промисах, просто дело в том, что последовательные циклы на промисах пишутся в разы сложнее, чем на async/await, тогда как последний прекрасно позволяет это сделать в читабельном и поддерживаемом формате.
И дело не в функциональщине vs императивщине, а в том, что оба подхода лучше сочетать, а не впадать в крайности.
Shannon
12.04.2017 00:32+1Все эти примеры выдуманы, чтобы показать упрощение синтаксиса, но вы зацепились за конкурентность
По сути, обычно следующий запрос может зависит от ответа предыдущего, а иногда и сразу от двух предыдущих запросов, поэтому конкурентность не требуется. А когда она требуется, то просто делается все через Promise.all
Вот упрощенный пример из рабочего проекта:
const [userdata, params] = await Promise.all([getUserData(), getParams()]) let bricks = [] if (params.allowedExtraData && userdata.needExtraData) { userdata.extraData = await getExtraData(userdata.id) } bricks.push(...params.bricksForModule[userdata.moduleType]) bricks.push(...params.bricksForType[params.mainType]) if (params.groups[userdata.groupType].isModerator) { bricks.push(...patams.templatesModerator) } const bricksData = await Promise.all(bricks) ...
И дальнейшая обработка результатовHaMI
12.04.2017 01:11-1у вас кажись бажина в коде проекта:
bricks.push(...patams.templatesModerator)
Shannon
12.04.2017 02:04+1Фраза «упрощенный пример» о чем нибудь говорит?
HaMI
12.04.2017 02:53+1нет – со стороны выглядит будто вы этот кусок вырезали из кода, не меняя имен переменных. Но, в принципе, не суть.
Не хотел вас поддеть или как-то обидетьShannon
12.04.2017 02:54Ну тоесть вы серьезно думаете, что кусок кода с необъявленной переменной может скомпилироваться?
HaMI
12.04.2017 03:01+1я ничего не думаю – увидел опечатку, сказал вам об этом. Вам достаточно было проигнорировать это или просто сказать – опечатка.
В следующий раз буду ставить много много смайликов – чтобы вы не подумали что я хочу вас обидеть
TheShock
12.04.2017 03:19кусок кода с необъявленной переменной может скомпилироваться?
Это ведь JavaScript, конечно может. Или не JavaScript?HaMI
12.04.2017 03:29ключевое слово здень – необъявленная. Тут можно выдумать каким образом это прокатило – но будет выглядеть натянуто, и скорее как оправдание.
TheShock
12.04.2017 04:31Не надо ничего выдумывать, оно просто будет корректно работать, это же JavaScript
Shannon
12.04.2017 03:46Это ведь JavaScript, конечно может. Или не JavaScript?
Так и есть, мой промах, не учел что этот кусок кода в условии, которое может никогда не выполнитсяbano-notit
12.04.2017 13:39Прикол в том, что даже если условие будет положительным вместо этой переменной подствиться undefined. А дальше уже соответствующие ошибки полезут, аля
undefined is not a function
,cannot read property <...> of undefined
.Shannon
12.04.2017 16:55Да нее, будет «ReferenceError: patams is not defined» в любом случае, даже если без 'use strict' запускать
Единственное что, если без 'use strict' запускать, можно записать какое-нибудь значение в необъявленную переменную, она в глобальную область видимости попадет, но само по себе undefined не подставится
Shannon
12.04.2017 03:45нет – со стороны выглядит будто вы этот кусок вырезали из кода
Как раз наоборот, код сократил раза в 3, а все переменные переименовал, чтобы легче смысл передать
HaMI
12.04.2017 02:40Я не понял посыл вашего комментария.
с точкой зрения:
>PS дихотомия между отверткой и молотком — ложная. Нужны оба инструмента!
я вполне согласен – ваш первый пример, если его вдумчиво «покурить», показал что есть ситуация в которой async/await упрощает жизнь. Но в статье об этом ни слова.
Про ваш второй пример – я такого сказать не могу. Ну заменили вы .then на await. Вроде как стало на один уровень вложености меньше, но стоит вам добавить обработку исключений и все вернестя на свои места.
то что вам не нужно пробрасывать через .then пары аргументов – это плюс.
Ну и он страно написан – await Promise.all(bricks). возможно вы где-то кусок кода удалили – но, даже так, это странно складывать промисы и что-то «статитеское» в один и тот же список. Скорее кто-то на всякий случай обернул чтобы не думать что там внутри. Есть еще вариант, что там в params промисы где-то лежат – но это тоже странно.
>По сути, обычно…
бывает по разному. давайте все же смотреть на картину в целом.
>А когда она требуется, то просто делается все через Promise.all
Если вас не затруднит, проилюстрируйте свою идею кодом — по, возможности, перепишите код из этого комментария
https://habrahabr.ru/company/ruvds/blog/326074/#comment_10166298 , само сабой – с учетом опечатки и без break так чтобы делало как можно больше запросов «паралельно»Shannon
12.04.2017 03:28И Остапа понесло…
Ну и он страно написан – await Promise.all(bricks)
bricks — это набор функции, которые возвращают промис.
Это модули, которые делают только свою маленькую работу, все они запускаются параллельно и их результат складывается в единый результат, а дальше в шаблонизаторе, уже в зависимости от наличия модуля будет активирован или нет нужный кусок шаблона
Ну заменили вы .then на await. Вроде как стало на один уровень вложености меньше, но стоит вам добавить обработку исключений и все вернестя на свои места.
В данном случае, обработка исключений одна единственная на более высшем уровне (на точке входа), на каждом этапе она не требуется
>А когда она требуется, то просто делается все через Promise.all
Вот этот пример: https://habrahabr.ru/company/ruvds/blog/326074/#comment_10167050
Если вас не затруднит, проилюстрируйте свою идею кодом — по, возможности, перепишите код из этого комментария
Но если вам нужен именно пример моего изначального примера:
Заголовок спойлераasync function gogogo(queryList) { let resultJson = {} let promiseList = [] let rawDataPromiseList = [] for (const query of queryList) { let json = {} const type = await getType(query) if (type === 1) { promiseList.push(getJson(query)) } else if (type === 2) { rawDataPromiseList.push(getRawData(query)) } else { break } } for (const rawData of await Promise.all(rawDataPromiseList)) { promiseList.push(rawToJson(rawData)) } for (const json of await Promise.all(promiseList)) { Object.assign(resultJson, json) } return resultJson }
И из цикла выйдем, и все запросы пойдут конкурентно, максимально насколько возможно в рамках данного примера. И даже больше, легко добавим более сложные условия:
const type = await getType(query) const subtype = await getSubType(query) if (type === 1 && subtype === 4) { promiseList.push(getJson(query)) } else if (type === 2 && !subtype) { rawDataPromiseList.push(getRawData(query)) } else if (subtype === 1) { break }
HaMI
12.04.2017 04:11>bricks — это набор функции, которые возвращают промис.
params.bricksForModule[userdata.moduleType] – тоесть это promise? или функция – если функция, то когда она вызывается?
За пример кода – спасибо. В принципе увидел, то что хотел.
Shannon
12.04.2017 04:24Да, это массив промисов, через рест параметр они вливаются в общий массив промисов, которые потом вызываются параллельно
HaMI
12.04.2017 04:35Типа комбинированный подход.
из плюсов:
не меняет порядок эллеметов( у вас promiseList «как бы упорядочен»)
const Promise = require('bluebird') function gogogo(queryList) { const parts = [] for (const query of queryList) { const type = await getType(query) if (type === 1) { parts.push(getJson(query)) } else if (type === 2) { parts.push( getRawData(query).rawToJson(rawData) ) } else { break } } return Promise .all(parts) .reduce(Object.assign, {}) }
В общем – спасибо за пример. Ну и всегда приятно пообщатся с человеком который подкрепляет свои суждения кодом. Приятных
mayorovp
12.04.2017 05:43только в действительности первый вариант будет выполнятcя ~n времени(конечно, с оговорками), а второй 3*n времени. И если эту разницу игнорировать – то нам вообще не нужен async/await(как и promise), нам просто нужно синхронное IO
Я ни коим образом не предлагаю эту разницу игнорировать! Напротив, я предлагаю совершенно другое: использовать async/await когда нужно последовательное выполнение и map + Promsie.all когда нужно параллельное.
k12th
Мне кажется, что комитет слишком увлекся синтаксическими плюшками в ущерб стандартной библиотеке. async/await это здорово, конечно, но для вменяемой работы с датами надо до сих пор ставить сторонний пакет, а ведь это гораздо проще стандартизировать и полифиллить, чем новый синтаксис. Проблемы с left-pad можно было легко избежать, если бы этот метод был в стандартной библиотеке.
Jaromir
> Мне кажется, что комитет слишком увлекся синтаксическими плюшками
Нет. На ES6+ писать намного приятней, чем на ES5. Порог вхождения, правда, существенно выше
> Проблемы с left-pad можно было легко избежать, если бы этот метод был в стандартной библиотеке
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart
k12th
Кто спорит?
Это можно и нужно было сделать еще в ES5, а попадет это только в ES2017.
monolithed
Для работы с датами есть Intl.DateTimeFormat и Date.prototype.toLocaleString. Базовые потребности они удовлетворяют более чем.
k12th
Да? И как там вывести дату в формате DD.MM.YYYY?
spmbt
Очень просто,
ну и там строчку в число перевести массивом немного: )Mingun
Это называется не «очень просто», а «жуткий костыль». Вот просто:
mayorovp
Сарказм-детектор проверьте, кажется он у вас поломался
monolithed
k12th
Офигенное API. Но вроде бы работает, справедливости ради.
monolithed
Можно еще короче:
spmbt
И ещё: Intl.DateTimeFormat().format(new Date)
Да, и вот как без неё:
mayorovp
Видите ли в чем дело — поставить сторонний пакет проще чем "поставить" синтаксическую плюшку. Поэтому синтаксичкские плюшки — это в языке самое важное.
kahi4
Для работы с датами стандартом de-facto стал moment.js, несмотря на корявость своего API (особенно его любовь мутировать даты). Меня уже давно мучает вопрос, почему просто не взять и не включить его как стандарт? Доработать API и красота.
k12th
Не уверен, что одобряю эту идею. Помнится, были предложения включать jQuery прямо в состав браузера — мол, все равно 95% сайтов его использует. Никто не стал этого делать, и слава богу.
arvitaly
Ну главную часть все же добавили, querySelector, даже переменная в некоторых браузерах та же ($).
TheShock
Разве?
mayorovp
Видимо, имелась в виду вспомогательная функция, видимая в инструментах разработчика.
oWeRQ
А также classList, map, each, Promises, formData и fetch, css анимации, да в общем-то почти все в том или ином виде застандартизировали, и слава богу, что не под копирку, только про цепочки вызовов забыли.
comerc
Возьмите date-fns :)
raveclassic
Только предложить хотел :)