В данной статье я на практическом примере расскажу о реализации способа, которым я воспользовался для решения моей задачи. Приступим.
Для реализации нам понадобятся: promises (я буду использовать библиотеку q), версия nodejs с поддержкой генераторов, подопытная асинхронная функция (будем использовать setTimeout).
var q = require("q");
function* itemGenerator(data)
{
var i, len = data.length;
for(i = 0; i < len; i++)
{
yield data[i];
}
}
function oldIterator(data)
{
var i = -1, len = data.length;
return {
"next": function()
{
i++;
return {
"done": i == len,
"value": data[i]
};
}
}
}
function main()
{
var def = q.defer(), items = [1, 2, 3, 4, 5];
(function foo(gen)
{
var genItem = gen.next(), item = genItem.value;
if(genItem.done)
{
def.resolve(true);
return;
}
console.log("start call for", item);
setTimeout(function()
{
console.log("end call for", item);
foo(gen);
});
})(itemGenerator(items))
return def.promise;
}
main().then(function(flag)
{
console.log("promise has been resolved", flag);
});
Результатом выполнения данного скрипта будет:
> node async_sync.js
start call for 1
end call for 1
start call for 2
end call for 2
start call for 3
end call for 3
start call for 4
end call for 4
start call for 5
end call for 5
promise has been resolved true
Как мы видим все наши асинхронные вызовы проходят в синхронном режиме. Для поддержки более старых версий(не поддерживающих генераторы), я добавил функцию “oldIterator”, которая будет работать аналогично генератору.
PS: данный код будет работать аналогично и в JavaScript, достаточно заменить библиотеку “Q” на родные “Promise”.
На этом все, спасибо за внимание!
Комментарии (25)
affair
30.08.2016 22:54Спасибо за статью.
А можно было данную проблему решить с помощью библиотеки async(http://caolan.github.io/async/)?Icebeer
30.08.2016 22:55-7Можно, но я предпочитаю не использовать сторонние библиотеки, если есть возможность решить задачу стандартными средствами.
bromzh
30.08.2016 23:38+6я предпочитаю не использовать сторонние библиотеки, если есть возможность решить задачу стандартными средствами.
Почему же тогда вы взяли q (где промисы не по es6), а не стандартные промисы?
Icebeer
31.08.2016 00:17-7Можно и стандарт, в примере привел кусок рабочего кода. И опять же, вы придираетесь :)
RockPresident
31.08.2016 16:11Просто для разъяснения: рабочий код — не значит хороший код.
Кроме функциональности, в коде имеет значение чтобы он был легко понимаем, легко поддерживался, имел минимум скрытых эффектов и багов, нестандартных решений которые потом могут аукнуться если код нужно будет кому-нибудь изменить или надстроить.
Стандарты и библиотеки как раз для этого и создаются.
mwizard
30.08.2016 23:32+12Наверное, пройдет еще лет 40, и люди начнут потихоньку использовать современный Javascript, но не везде и не полностью, ведь все еще нужно поддерживать IE5.5.
Автор, у вас вместо кода дикое, мерзкое, нечитаемое месиво вместо кода.
import Promise from 'bluebird'; async function process(item) { console.log(`start call for ${item}`); await Promise.delay(1000); console.log(`end call for ${item}`); } async function main() { const items = [1, 2, 3, 4, 5]; for (let item of items) { await process(item); } } main().then(() => { console.log(`done`); });
Icebeer
31.08.2016 00:16-7Спасибо, что поделились, но статья не об этом!
mwizard
31.08.2016 00:25+9Правильно — вы планировали статью "ах, какой я молодец", а получилась статья "как написать говно на JS и не подать виду".
Вы вместо простого и элегантного подхода выбрали самый сложный и неправильный — вместо new
Promise((resolve, reject) => { ... })
, как рекомендует спецификация, вы используете defer, который мало того, что тянет за собой огромный, медленный Q, еще и стандарту не соответствует.
Нагородили итератор и генератор поверх массива, вместо
for (let x of y) { ... }
, который нода, между прочим, поддерживает уже очень давно, даже без дополнительных флагов. Сделали мешанину анонимных функций в теле main. Зачем?!.. Зачем там замыкание, вы объясните? На что оно замыкается? Нахрена ему это делать прямо в main при каждом вызове?
Родина вам async/await дала — пиши. Не хочу, хочу setTimeout. И это программисты? Какая может быть уважительная причина их не использовать?
deniskreshikhin
31.08.2016 10:51+7"как написать говно на JS и не подать виду".
Нахрена ему это делать прямо в main при каждом вызове?Зачем же столько агрессии?
Разве нельзя все тоже самое написать без грубых слов, оскорблений и перехода на личности?
Я уж не говорю что это серьезно нарушает правила хабр, но и даже читать неприятно.
Все-таки любая истина требует обсуждения и консенсуса. Тем более в программировании. Но какое может быть обсуждение и консенсус, когда один участник оскорбляет другого? Своим поведением вы только мешаете разобраться в деле другим хабровчанам.
raveclassic
31.08.2016 00:24+1Ну, так то, можно еще и промисы не импортить
mwizard
31.08.2016 00:29+1Импортнул из двух соображений. Первое — готовая реализация
Promise.delay()
:
function delay(timeout) { return new Promise((resolve, reject) => setTimeout(resolve, timeout)); }
Второе — Bluebird в несколько раз быстрее нативных промисов, поэтому имеет смысл его использовать даже там, где промисы поддерживаются нативно.
raveclassic
31.08.2016 00:36+1Да, bluebird быстрее, но async/await — это очень медленно (в случае regenerator'а), чуть менее медленно в случае генераторов (если в ноде). Есть ли смысл тогда сочетать bluebird с async/await?
mwizard
31.08.2016 00:45+1Ну использование regenerator в принципе обоснованно только для очень старых платформ, которые не поддерживают генераторы, а там уже о скорости никто не думает — да и нативных промисов там в таком случае тоже отродясь не было, поэтому сравнивать не с чем. Для окружений возрастом хотя бы в пару лет уже имеет смысл использовать исключительно генераторы.
А о том, что генераторы медленнее промисов — у вас есть какие-то материалы по теме? Быстрый гуглеж показал результаты противоположные вашему заявлению, но я их не верифицировал.
raveclassic
31.08.2016 00:54+1Материалов, увы, нет. Наверное, все ж, сложившееся (само собой, стереотипное) мнение. А как там дела у V8 в бенчах вообще трудно смотреть, он, зараза, оптимизирует все, попробуй правильный бенч напиши еще.
Кстати, залез сейчас посмотрел поддержку генераторов — остался приятно удивлен. Но, увы, бизнес в моем случае требует мобайл (ios9), так что, все-равно, придется фоллбечить. А мобилки — и так черепахи.
pikko
31.08.2016 00:48действительно, асинхронность первое время немного выносит мозг.
Если надо несколько раз последовательно вызвать асинхронную функцию —
можно их просто chain'ить на результат предыдущей:
const Q = require('q'); var d = Q(1); for ( var i = 0; i < 5; i++ ) (function(i){ d = d.then(function(){ return somethingThatReturnsPromise(i); }); })(i)
alQlagin
31.08.2016 07:11+1Минусующие поясните в чем тут проблема. С async/await подход конечно лучше, но чейн промисов вроде нормальная практика? А Q здесь можно заменить любой другой реализацией.
mwizard
31.08.2016 15:02На мой взгляд — вполне нормальная практика. Плюс, большинство библиотек так или иначе предоставляют утилитарный метод для этого, куда можно просто скормить массив промисов.
Laney1
31.08.2016 11:58непонятно, к чему эта статья. Эмуляция таймаутов и прочих async/await с помощью генераторов — стандартный прием, описанный например тут: https://learn.javascript.ru/generator#плоский-асинхронный-код
Yavanosta
Вы сделали обычную очередь.
Не вижу преобразования асинхронного кода в синхронный. Это вообще невозможно в общем случае.
Icebeer
Ну как же, есть цикл запуска асинхронных функций, которые были запущены последовательно. Возможно я не совсем верно отразил это в заголовке.