Итак, не так давно мне пришлось столкнуться с довольно популярной задачей преобразования асинхронного кода в синхронный в рамках цикла. В моем случае это была работа с AmazonAPI методами productSearch. Все бы ничего, да вот только данный API очень не любит когда к нему обращаются слишком часто, а мне было необходимо в цикле опрашивать состояние продуктов.

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

Для реализации нам понадобятся: 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)


  1. Yavanosta
    30.08.2016 22:50
    +5

    Вы сделали обычную очередь.

    Не вижу преобразования асинхронного кода в синхронный. Это вообще невозможно в общем случае.


    1. Icebeer
      30.08.2016 22:56

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


  1. affair
    30.08.2016 22:54

    Спасибо за статью.
    А можно было данную проблему решить с помощью библиотеки async(http://caolan.github.io/async/)?


    1. Icebeer
      30.08.2016 22:55
      -7

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


      1. bromzh
        30.08.2016 23:38
        +6

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

        Почему же тогда вы взяли q (где промисы не по es6), а не стандартные промисы?


        1. Icebeer
          31.08.2016 00:17
          -7

          Можно и стандарт, в примере привел кусок рабочего кода. И опять же, вы придираетесь :)


          1. RockPresident
            31.08.2016 16:11

            Просто для разъяснения: рабочий код — не значит хороший код.
            Кроме функциональности, в коде имеет значение чтобы он был легко понимаем, легко поддерживался, имел минимум скрытых эффектов и багов, нестандартных решений которые потом могут аукнуться если код нужно будет кому-нибудь изменить или надстроить.
            Стандарты и библиотеки как раз для этого и создаются.


  1. 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`);
    });
    


    1. Icebeer
      31.08.2016 00:16
      -7

      Спасибо, что поделились, но статья не об этом!


      1. mwizard
        31.08.2016 00:25
        +9

        Правильно — вы планировали статью "ах, какой я молодец", а получилась статья "как написать говно на JS и не подать виду".


        Вы вместо простого и элегантного подхода выбрали самый сложный и неправильный — вместо new Promise((resolve, reject) => { ... }), как рекомендует спецификация, вы используете defer, который мало того, что тянет за собой огромный, медленный Q, еще и стандарту не соответствует.


        Нагородили итератор и генератор поверх массива, вместо for (let x of y) { ... }, который нода, между прочим, поддерживает уже очень давно, даже без дополнительных флагов. Сделали мешанину анонимных функций в теле main. Зачем?!.. Зачем там замыкание, вы объясните? На что оно замыкается? Нахрена ему это делать прямо в main при каждом вызове?


        Родина вам async/await дала — пиши. Не хочу, хочу setTimeout. И это программисты? Какая может быть уважительная причина их не использовать?


        1. deniskreshikhin
          31.08.2016 10:51
          +7

          "как написать говно на JS и не подать виду".
          Нахрена ему это делать прямо в main при каждом вызове?

          Зачем же столько агрессии?
          Разве нельзя все тоже самое написать без грубых слов, оскорблений и перехода на личности?


          Я уж не говорю что это серьезно нарушает правила хабр, но и даже читать неприятно.


          Все-таки любая истина требует обсуждения и консенсуса. Тем более в программировании. Но какое может быть обсуждение и консенсус, когда один участник оскорбляет другого? Своим поведением вы только мешаете разобраться в деле другим хабровчанам.


    1. raveclassic
      31.08.2016 00:24
      +1

      Ну, так то, можно еще и промисы не импортить


      1. mwizard
        31.08.2016 00:29
        +1

        Импортнул из двух соображений. Первое — готовая реализация Promise.delay():


        function delay(timeout) {
            return new Promise((resolve, reject) => setTimeout(resolve, timeout));
        }

        Второе — Bluebird в несколько раз быстрее нативных промисов, поэтому имеет смысл его использовать даже там, где промисы поддерживаются нативно.


        1. raveclassic
          31.08.2016 00:36
          +1

          Да, bluebird быстрее, но async/await — это очень медленно (в случае regenerator'а), чуть менее медленно в случае генераторов (если в ноде). Есть ли смысл тогда сочетать bluebird с async/await?


          1. mwizard
            31.08.2016 00:45
            +1

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


            А о том, что генераторы медленнее промисов — у вас есть какие-то материалы по теме? Быстрый гуглеж показал результаты противоположные вашему заявлению, но я их не верифицировал.


            1. raveclassic
              31.08.2016 00:54
              +1

              Материалов, увы, нет. Наверное, все ж, сложившееся (само собой, стереотипное) мнение. А как там дела у V8 в бенчах вообще трудно смотреть, он, зараза, оптимизирует все, попробуй правильный бенч напиши еще.
              Кстати, залез сейчас посмотрел поддержку генераторов — остался приятно удивлен. Но, увы, бизнес в моем случае требует мобайл (ios9), так что, все-равно, придется фоллбечить. А мобилки — и так черепахи.


    1. i360u
      31.08.2016 10:56
      +1

      Этот пост стоило написать ради вашего комментария (не сарказм)


  1. TimsTims
    31.08.2016 00:02

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


    1. Icebeer
      31.08.2016 00:15
      -4

      С радостью посмотрю ваш способ решения подобной задачи.


      1. mwizard
        31.08.2016 00:30
        +3

        А чем способ комментарием выше не устроил?


    1. mwizard
      31.08.2016 00:31
      +5

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


  1. 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)
    
    


    1. alQlagin
      31.08.2016 07:11
      +1

      Минусующие поясните в чем тут проблема. С async/await подход конечно лучше, но чейн промисов вроде нормальная практика? А Q здесь можно заменить любой другой реализацией.


      1. mwizard
        31.08.2016 15:02

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


  1. Laney1
    31.08.2016 11:58

    непонятно, к чему эта статья. Эмуляция таймаутов и прочих async/await с помощью генераторов — стандартный прием, описанный например тут: https://learn.javascript.ru/generator#плоский-асинхронный-код