Мои контакты


понедельник, 22 сентября 2014 г.

Шаблон Deferred Object

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


Описание проблемы


Вообще, подобные задачи на практике встречаются нечасто. Я столкнулся с ней при работе с API vk.com, когда мне нужно было получить посты из нескольких публичных страниц, потом их все вместе как-то обработать и показать некоторую выборку.

Вызов метода API выглядит примерно так:

VK.Api.call('wall.get', params, callback);

Callback вызывается асинхронно, когда приходит ответ от сервера VK. Понятно, что при таком подходе я смогу обрабатывать данные только частями - посты отдельной страницы VK. Мне такой вариант не подходил, т.к. все посты нужно было анализировать вместе. Поэтому я стал думать, как "дождаться" пока придут все ответы на запросы и сразу их обработать.

Решение


Я понимал, что наверняка для этой проблемы существует решающий ее шаблон. Так и оказалось, этот шаблон - Deferred Object. По сути, deferred object просто хранит состояние асинхронной функции. Их может быть три:
  • pending, т.е. ожидание завершения
  • rejected, т.е. выполнение завершено неудачно
  • resolved, т.е. выполнено успешно

Таким образом, чтобы использовать этот шаблон, мы должны создать Deferred object, передать его туда, где его состояние будет отслеживаться, а когда асинхронная функция будет выполнена, перевести его из состояния pending в состояние resolved или rejected.

В jQuery уже есть своя реализация Deferred Object - $.Deferred. Можно ей и воспользоваться. Я положу все состояния асинхронных запросов в одну коллекцию и дождусь, когда все они перейдут в resolved.

var def, 
    posts = [],
    deferred = [];

for (var i = 0; i < publics.length; ++i) {
   def = (function(params) {
      var defObj = new $.Deferred();

      VK.Api.call('wall.get', params, function(r) {
         if (r != undefined && r.response != undefined) {
            posts.push(r.response);
         }

         defObj.resolve();
      });

      return defObj.promise();
   })(publics[i].params);

   deferred.push(def);
}

$.when.apply($, deferred).done(function() {
   // processing ...
});

Код немного подправлен для наглядности. Объясню, что произошло. Перед отправкой каждого асинхронного запроса, я создаю Deferred Object, который переводится в состояние Resolved только когда приходит ответ. Все созданные объекты помещаются в общий массив, который потом передается функции $.when, где происходит ожидание, пока все deferred objects не перейдут в состояние Resolved.

Стоит обратить внимание на то, что в массив deferred попадают не сами $.Deferred, а Promised-версия Deferred Object. По сути, это просто "урезанная" копия Deferred Object, которая не позволяет изменить свое состояние вызовом у нее специального метода.

Для вызова функции $.when, я использовал Function.prototype.apply, которая переводит переданный массив объектов в список аргументов, т.к. в документации $.when описан как метод, принимающий список параметров, а не массив.

Ссылки