Ещё раз о Deferred/Promise

  • Tutorial
DeferredТак получилось, что последние несколько недель очень часто приходилось слышать слова Promise и Deferred от разных людей. Как правило, этими понятиями оперируют уже повидавшие виды разработчики, столкнувшиеся в своей деятельности с определенными задачами.

Как я могу судить, для людей, которые на практике не столкнулись с некоторыми специфическими проблемами, эти 2 понятия являются довольно трудными для понимания. И не потому, что понятия Promise и Deferred являются с чем-то сложным, а потому, что довольно непросто сходу выдумать подходящую задачу, чтобы попробовать Deferred objects (в JQuery и не только) в действии.

Да, вероятно для тех, кто знаком с этим вопросом он покажется пустяковым и не стоящим и выеденного яйца. Кроме того, вопрос уже многократно обсуждался. Однако, я наберусь смелости еще раз его затронуть и вот почему: 1) Мне кажется, что для некоторых читателей этот пост может оказаться интересным. 2) Я пойду от практики, а не от теории. Моя задача — продемонстрировать работу инструмента. Теорию и другие варианты применения при необходимости вы найдете в ссылках к посту.

Ниже я попробую показать вам что Promise и Deferred это очень и очень просто. Кроме того, для объяснения этой темы, мне придётся затронуть еще несколько интересных моментов JavaScript.



Что такое Deferred object?: это весьма простой способ отлавливать состояния асинхронных событий.
Зачем их использовать?: например, что бы назначить общий колбэк нескольким AJAX запросам.
Где это можно использовать?: например, вы хотите показать на странице карту. Но только тогда, когда получили информацию обо всех отметках на ней из внешнего источника. Предположим, вы загружаете страницу и карту (в скрытом виде) и показываете прелоадер, в этот момент браузер отправляет на ваш сервер 20 AJAX запросов и получает информацию. И только по завершении всех запросов вам требуется скрыть прелоадер, показать карту, маркеры и блоки контента. Вот так приблизительно можно сформулировать абстрактную задачу.

Далее я буду показывать примеры на CoffeeScript (c JavaScript под спойлером) и мне следует пояснить некоторые моменты, касающиеся CoffeeScript.

1) CoffeeScript это абсолютно тот же JavaScript, но с более чистым и лаконичным синтаксисом.
2) -> в CoffeeScript означает function(){ } в JavaScript
3) (a, b) -> в CoffeeScript означает function(a, b){ } в JavaScript
4) do в CoffeeScript означает () в JavaScript. Т.е вызов функции, имя которой указано после do
5) do (a, b) -> "Hello World!" в CoffeeScript означает (function(a, b){ return "Hello World!"; })(a, b) в JavaScript

Метод apply

Я ленивый и не люблю лишний раз стучать пальцами по клавишам. Поэтому console.log я предпочитаю использовать просто log. Этого легко добиться используя JS метод apply.

Что такое метод apply? Это один из методов Function (Function.apply). А значит, он применим ко всем функциям.
Что делает метод apply? Он вызывается от функции и следовательно влияет на нее. Первый аргумент задает контекст вызываемой функции, т.е. фактически принудительно устанавливает значение this внутри этой функции. Второй аргумент задает массив параметров для функции.
Где это можно использовать? Часто в описании функций можно увидеть, что она принимает на вход список параметров fn(x1, x2, x3, x4, ...), но по странному стечению обстоятельств у вас может оказаться массив переменных и вам бы хотелось передать этот массив в функцию в виде списка, а не одной переменной-массивом. Именно это и помогает сделать метод apply. По сути apply позволяет развернуть массив переменных в список параметров функции.

Зная то, что массив всех значений переданных в функцию в JavaScript можно получить через переменную arguments, сделаем следующий финт ушами:

window.log = -> try console.log.apply(console, arguments)


JS версия
window.log = function() {
  try {
    return console.log.apply(console, arguments);
  } catch (_error) {}
};




Что здесь произошло? Я создал в глобальной области видимости метод log. Все аргументы, которые я получаю (arguments) при вызове метода log я передаю в виде списка функции console.log (сделать мне помогает метод apply). Кроме того, что бы не нарушить логику работы метода console.log, я предпочел передать в качестве первого аргумента в функцию apply объект console. Для браузеров, у которых есть проблемы с console.log я использую try, что бы обезопасить себя.

Еще немного сахара в Coffee
Поставьте ... после массива arguments и вы избавите себя от указания контекста и ручного написания apply. Данный код совершенно идентичен представленному выше.

window.log = -> try console.log arguments...




Если с этим все более-менее понятно, то движемся дальше. Кстати, я не просто так вспомнил про apply — он нам еще пригодится.

Шаг 1. Классическая проблема

Для эмуляции 10 асинхронных запросов используем метод setTimeout. При выполнении он отрывается от основного потока исполнения и «живет своей жизнью». Вот мы и нашли отличный вариант для проверки работы Deferred объектов.

Сделаем простой цикл и запустим setTimeout 10 раз и прологгируем переменную index.

for index in [0...10]
  setTimeout ->
    log index
  , 1000


JS версия
var index, _i;

for (index = _i = 0; _i < 10; index = ++_i) {
  setTimeout(function() {
    return log(index);
  }, 1000);
}



Ровно через секунду мы получим не числа «0 1 2 3 4 5 6 7 8 9», а «10 10 10 10 10 10 10 10 10 10».
Произошло это потому, что к тому момент когда выполнится код внутри setTimeout, цикл уже закончится (он отработает очень быстро), а счетчик index к этому моменту примет крайнее значение и будет равен 10.

Шаг 2. Классическое решение

Для того, что бы решить вышеуказанную проблему, вам достаточно обернуть исполняемый код в функцию и вызвать её, передав текущее значение счетчика. Вуаля! В кофескрипте эти действия выполняются довольно элегантно.

for index in [0...10]
  do (index) ->
    setTimeout ->
      log index
    , 1000


JS версия
var index, _fn, _i;

_fn = function(index) {
  return setTimeout(function() {
    return log(index);
  }, 1000);
};
for (index = _i = 0; _i < 10; index = ++_i) {
  _fn(index);
}



Шаг 3. Как работает deferred?

Deferred объект — это всего лишь хранилище состояния асинхронной функции. Таких состояний обычно несколько:

pending — ожидание завершения процесса
rejected — процесс закончен падением
resolved — процесс закончен успешно

Кроме того у Deferred объекта есть ряд методов, которые могут менять его состояние. Например, метод .resolve().

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

Из этого мы сможем сделать вывод, что для использования Deferred объекта, мы должны:

1) создать Deferred объект
2) При завершении асинхронного метода перевести Deferred объект в нужное состояние
3) Передать Deferred объект из текущей функции куда-то, где его состояние будут отслеживать.

Ниже вы видите код, который это иллюстрирует. Подробный разбор я оставляю на откуп заинтересованному читателю. Про return dfd.promise() читайте сразу после примера кода.

for index in [0...10]
  promise = do (index) ->
    dfd = new $.Deferred()
    
    setTimeout ->
      log index
      dfd.resolve()
    , 1000

    return dfd.promise()


JS версия
var index, promise, _i;

for (index = _i = 0; _i < 10; index = ++_i) {
  promise = (function(index) {
    var dfd;
    dfd = new $.Deferred();

    setTimeout(function() {
      log(index);
      return dfd.resolve();
    }, 1000);

    return dfd.promise();
  })(index);
}



Вы обязательно должны обратить внимание на строку return dfd.promise(). И наверняка вы зададите резонный вопрос: «почему при возврате из функции передается не сам Deferred объект, а именно Promise?». Суть в том, что считается, что при передаче Deferred объекта наружу, вы должны исключить из него все методы, которые могут изменить его состояние. Поскольку нигде, кроме строго заданной разрешенной области у Deferred объекта не должно быть возможности изменить свое состояние. Иначе, конечный разработчик может не устоять перед соблазном и нарушить заданный паттерн. Promise как раз и занимается тем, что возвращает урезанную по функционалу копию Deferred.

Шаг 4. Формируем массив «обещаний»

Если нам требуется дождаться выполнения 10 асинхронных функций — мы просто сформируем массив из «обещаний» (видимо обещаний когда-то закончить своё исполнение), которые формируются в функциях внутри цикла. И так, создаем пустой массив и заносим в него новое «обещание» на каждой итерации.

promises_ary = []

for index in [0...10]
  promise = do (index) ->
    dfd = new $.Deferred()
    
    setTimeout ->
      log index
      dfd.resolve()
    , 1000

    dfd.promise()

  promises_ary.push promise 

log promises_ary


JS версия
var index, promise, promises_ary, _i;

promises_ary = [];

for (index = _i = 0; _i < 10; index = ++_i) {
  promise = (function(index) {
    var dfd;
    dfd = new $.Deferred();

    setTimeout(function() {
      log(index);
      return dfd.resolve();
    }, 1000);

    return dfd.promise();
  })(index);

  promises_ary.push(promise);
}

log(promises_ary);


В итоге log покажет нам список объектов.

# => [obj, obj, ...]


Шаг 5. Используем массив «обещаний»

А теперь очень просто повесить колбэк, который будет вызван только после выполнения всех асинхронных функций. Для этого нам потребуется JQuery метод $.when. В документации $.when описан как метод принимающий на вход список параметров. Однако, мы на входе имеем массив параметров promises_ary. Как не трудно догадаться мы вновь используем метод apply.

promises_ary = []

for index in [0...10]
  promise = do (index) ->
    dfd = new $.Deferred()
    
    setTimeout ->
      log index
      dfd.resolve()
    , 1000

    dfd.promise()

  promises_ary.push promise 

$.when.apply($, promises_ary).done ->
  log 'Promises Ary is Done'


Шаг 6. Немного случайности

В конце, конечно, следует добавить немного «случайности» нашей функции setTimeout. Сделаем простой хелпер метод, который будет возвращать случайные числа и будем со случайной задержкой выполнять код внутри setTimeout.

rand = (min, max) -> Math.floor(Math.random() * (max - min + 1) + min)

promises_ary = []

for index in [0...10]
  promise = do (index) ->
    dfd = new $.Deferred()
    
    setTimeout ->
      log index
      dfd.resolve()
    , rand(1, 5) * 1000

    dfd.promise()

  promises_ary.push promise 

$.when.apply($, promises_ary).done ->
  log 'Promises Ary is Done'


JS версия
var index, promise, promises_ary, _i;

rand = function(min, max) {
  return Math.floor(Math.random() * (max - min + 1) + min);
};

promises_ary = [];

for (index = _i = 0; _i < 10; index = ++_i) {
  promise = (function(index) {
    var dfd;
    dfd = new $.Deferred();
    
    setTimeout(function() {
      log(index);
      return dfd.resolve();
    }, rand(1, 5) * 1000);

    return dfd.promise();
  })(index);

  promises_ary.push(promise);
}

$.when.apply($, promises_ary).done(function() {
  return log('Promises Ary is Done');
});



В завершении следует сказать, что все AJAX запросы в JQuery с версии 1.5 используют механизм Deferred/Promise, поэтому работа с ними еще больше упрощается.

На этом я остановлюсь. Базовый пример с объяснениями, на мой взгляд, завершен. Думаю этого будет достаточно, что бы продолжить более глубокое изучение техники Deferred/Promise по другим источникам. Надеюсь этот пост однажды кому-то поможет.

  1. Deferred Object (JQuery official)
  2. jQuery Deferred Object (Habr)
  3. Использование Deferred объектов в jQuery 1.5 (Habr)
  4. CommonJS Promises
  5. AngularJS (promise/deferred)
  6. Deferred объекты в AngularJS
  7. Promise-ы в AngularJS
  8. Promises for ActionScript 3.0
  9. Объект deferred


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

Вопрос к обсуждению: Было бы интересно узнать о тех задачах, где вы использовали Deferred/Promises.
Поделиться публикацией
Похожие публикации
AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама
Комментарии 28
  • +16
    Лучше сразу на JS бы показывали. Зачем использовать два языка, при чём более популярный скрывать под спойлеры?
    • +5
      Логично. Но 1) В тексте мой исходник. Предпочел оставить его, поскольку генерируемый JS хоть и понятен, но стилистически «машинный». В JS для большей наглядности некоторые вещи я бы оформил иначе. 2) Пропаганда CoffeeScript :)
    • +2
      Не редко приходится встречать утверждение, что Deferred может использоваться не только вкупе с асинхронными функциями, но и с синхронными. На данный момент я не встречал задач, где это может пригодиться.

      Это может пригодиться, если на данный момент вы работаете с синхронным API, но у вас есть подозрение (или желание), что через какое-то время придется перейти на асинхронное. Например, сейчас храните данные в localStorage, но есть желание попробовать IndexedDB.
      • 0
        Спасибо. Вы подтвердили мои предположения.
        • +1
          Именно по этому во всех promise библиотеках есть враппер функции, который проверяет вернула ли она promise или результат.

          Пример:
          a = ->
            123
          
          b = ->
            dfd = new $.Deferred()
            dfd.resolve 123
            dfd.promise()
          
          # работа с синхронной функцией
          $.when(a).then ->
          # аналогична работе с promise
          $.when(b).then ->
          
        • +5
          Уж извините, статья полезная, но поставил 0, т.к. CoffeeScript режет глаза.

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

          Для себя делал вот такой скрипт: github.com/lampaa/AsyncMarks

          Штука получилась прикольная, берет Deferred своей простотой.
          • +3
            Спасибо за ваше мнение. Да, знаю что JS разработчики часто не принимают CoffeeScript, но мне захотелось иллюстрировать происходящее именно на Coffee, поскольку именно его использую в повседневной практике. Вероятно, это было не популярное решение, но с моей точки зрения целесообразное.
            • 0
              Нужно было бы (и стоит) сделать наоборот: показывать JS, а Coffee под спойлерами. Те, кому интересно (а это 99% аудитории) посмотрели бы, прельстились бы и попробовали бы. А сейчас он только всех раздражает и эффект получился противоположный. Вплоть до того, что я для своего применения сам обязательно так и сделаю, выкинув этот кофескрипт нахрен :)

              Ну и, конечно, «генерируемый JS отдаёт нечеловечиной» — это гнилой отмаз. Отредактируйте его, сделайте человечнее.
              • +3
                Я вас услышал. Да, доля правды в ваших словах есть. Оспаривать не буду. Однако, я оформил статью так, как хотел бы прочитать ее я сам. Мне здесь нечего добавить. И отмазов никаких не было, выше я написал «В JS для большей наглядности некоторые вещи я бы оформил иначе». И я сделал бы это без каких-либо трудностей, если бы хотел. Но не хочу. Мне кажется все так, как должно быть. Мне жаль, что широкой публике это может быть не по нраву, но увы, у каждого из нас свое виденье.
                • +1
                  Ваша позиция понятна, вам нравится CoffeeScript. Но всё же, JavaScript знают и понимают все, а кофе — это просто сахар. Сегодня вы написали статью с использованием CoffeeScript, завтра Вася написал статью с использованием ClosureScript, послезавтра — с RedScript. Интересно будет?
                  • +1
                    Уж не знаю, как вам, а мне было бы очень интересно.
                    • +1
                      1) Мое мнение, что более чистый и лаконичный синтаксис КофеСкрипт идеально подходит для иллюстрирования идеи. Не загромождая код, можно показать самое важно. 2) Мне кажется, что затронутая тема может быть интересна не только JS разработчикам, а в таком случае «псевдокод» им будет читать еще и легче, чем нативный JS. 3) Если вы меня спрашиваете о том, приятно ли было бы мне читать JS в другом синтаксисе, то я вам отвечу следующее — да, мне было бы интересно прочитать примеры на, том же ClosureScript, если бы я видел во что они превратятся в итоге. Суть статьи все равно не в коде.
                      • +2
                        1) Мое мнение, что более чистый и лаконичный синтаксис КофеСкрипт идеально подходит для иллюстрирования идеи.

                        Ну для тех кто пишет на Ruby, это возможно и так. По мне, так это насилие над программистом, а не псевдокод. Хоть я когда-то давно и брался за CoffeScript то сейчас вот просто взять и прочитать примеры не смог, лез под спойлер.
                        (кстати, бросил CoffeScript и перешел на TypeScript именно из-за того что он понятнее читается. Там меньше мусора, который любят выдавать за синтаксический сахар)

                        • +2
                          Вы правы. Действительно, если бы я рассчитывал на широкую аудиторию то, использование кофескрипт было бы просчетом. Однако, я во-первых писал статью так, как хотел бы прочитать ее я сам. А во-вторых — целевая аудитория, которую я наметил — именно приверженцы руби. Тем не менее, я постарался дать и другим читателям возможность читать более привычный для них код. На этом предлагаю закрыть тему.
            • +1
              По опыту могу сказать, что Deferred/Promise (особенно из jQuery) очень сложно отлаживать. Везде, где можно обойтись без Promise лучше написать код на чистом es.
              Вот как я бы написал первые ваши примеры (там где можно обойтись без Promise):

              window.log = () => console.log(...arguments);
              
              for (var _i = 0; _i < 10; ++_i) {
                let index = _i;
                setTimeout(() => log(index), 1000);
              }
              

              Статья хорошая, но не хватает примеров с when, then, reject, fail и т.д. — ну чего-то более сложного про Deferred/Promise
              • +1
                И, кстати да, в CoffeeScript тоже есть спреды
                window.log = -> try console.log arguments...
                
                • +1
                  Вам под спойлер «Еще немного сахара в Coffee». Эту штуку я оставил там как опциональную. Спасибо за замечание )
                • 0
                  Чисто теоретически, это излечимая проблема. В WinRT была аналогичная проблема с отладкой асинхронных тасков (в т.ч. в приложениях на JS), и ее решили в VS 2013. Нет причин, по которым нельзя сделать то же самое для ноды.
                • +2
                  Спасибо, пригодится! Как раз недавно лепил костыль для подобного случая.
                  • +2
                    Возможно, многим будет удобнее использовать Async, как на клиенте так и на сервере (Node.js).
                  • +1
                    Кстати, вместо setTimeout, в КофеСкрипте лучше смотрится его алиас after = (ms, fn) -> setTimeout(fn, ms):

                    after 1000, ->
                      log index
                      dfd.resolve()
                    
                    • +1
                      Хорошее использование Deffered — ajaxQueue, плагин для жиквери, который создает очереди из ajax-запросов. Вот ответ на SO, в котором плагин и появился. Я его слегка модифицировал, добавив возможность вставлять запросы в самое начало очереди для очень важных данных.
                      • 0
                        Столкнулся с проблемой реализации очереди запросов. Т.е. выполняется один запрос (не важно с ошибкой или без), запускается второй, затем третий и т.д. Причем запросов таких неизвестное количество. Пока запускаю deferred в рекурсии. Может быть есть способ лучше?
                        • 0
                          Привет из года 2015.
                          Пишу на CoffeeScript. Читать статьи с примерами на JavaScript становится всё ленивее, т.к. ну очень много синтаксического мусора, которого CoffeeScript лишён. Да и мозги уже думают компактнее.

                          За статью спасибо огромное. Тема весьма обширная и требует практики. Полгода использую promises для построения логики игрушек, и только сейчас обратил внимание на весьма важную фичу — пробрасывание исключений по цепочке, позволяющее прервать цепочку в любом месте — от которой вначале избавлялся распихивая .catch везде где возможно для скорейшего вывода ошибки в консоль.

                          За CoffeeScript отдельное спасибо.
                          • 0
                            рад что пригодилось
                          • +1
                            … А во-вторых — целевая аудитория, которую я наметил — именно приверженцы руби.

                            После лаконичного руби нативный JS кажется излишне многословным. К сожалению или к счастью, но это так.
                            Спасибо! Отличная статья.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.