Пользователь
0,0
рейтинг
3 февраля 2011 в 08:32

Разработка → jQuery Deferred Object (подробное описание) из песочницы

31 января вышел релиз jQuery 1.5, одним из ключевых нововведений которого стал инструмент Deferred Object. Именно о нём я и хочу рассказать подробнее в этой статье.

Эта новая функциональность библиотеки направлена на упрощение работы с отложенными (deferred) вызовами обработчиков (callbacks). Deferred Object, аналогично объекту jQuery, «цепочный» (chainable), но имеет свой набор методов. Deferred Object способен регистрировать множество обработчиков в очередь, вызывать зарегистрированные в очереди обработчики и переключать состояние на «завершено» или «ошибка» для синхронных или асинхронных функций.

Общее описание


Идея довольно проста: необходимо создать Deferred Object, отдать этот объект некоторому внешнему коду, который может прицепить к нему обработчики (а может и не прицепить), а затем инициировать выполнение этих обработчиков вызовом всего одного метода объекта.

Весь процесс использования Deferred Object можно разделить на три глобальных этапа:
  • Создаётся и инициализируется объект deferred, состояние которого «не выполнялось».
  • Полученный объект выдаётся внешнему коду, где к объекту в очередь добавляются обработчики методами deferred.then(), deferred.done() и deferred.fail(). Обработчики могут быть двух типов: «обработка выполнения» и «обработка ошибки».
  • По выполнению некоторого условия (события) в коде, где был создан объект, вызывается метод выполнения обработчиков: deferred.resolve() вызывает обработчики «успешного завершения» и переводит состояние объекта в «выполнено», deferred.reject() вызывает обработчики «ошибки» и переводит состояние объекта в «отменено».

Слово «очередь» в описании Deferred Object используется не просто так — обработчики, добавленные в очередь, вызываются в том порядке, в котором они были добавлены (кого раньше добавили, тот раньше будет вызван).

При добавлении обработчиков совсем необязательно проверять состояние объекта deferred (а вдруг он уже выполнен или отменен). Если обработчик «выполнения»/«ошибки» добавляется к уже «выполненному»/«отменённому» объекту, то он будет вызван немедленно.

Повторный вызов resolve/reject у объекта deferred не приводт к изменению его состояния и повторному вызову обработчиков, а просто игнорируется (и не важно, что было вызвано до этого resolve() или reject()).

Deferred Object «цепочный», это означает, что его методы (не все) возвращают результатом ссылку на исходный объект, что даёт возможность вызывать несколько методов объекта подряд, разделяя их точкой. Например, так: deferred.done(callback1).fail(callback2);

Описание методов


deferred.done( doneCallbacks )
добавляет обработчик, который будет вызван, когда объект deferred перейдёт в состояние «выполнено»

deferred.fail( failCallbacks )
добавляет обработчик, который будет вызван, когда объект deferred перейдёт в состояние «отменено»

deferred.then( doneCallbacks, failCallbacks )
добавляет обработчики сразу обоих типов, описанных выше, эквивалентна записи deferred.done(doneCallbacks).fail(failCallbacks)

В описанных выше трёх методах в качестве аргументов doneCallbacks, failCallback могут выступать отдельные функции или массивы функций.

deferred.resolve( args )
переводит deferred объект в состояние «выполнено» и вызывает все обработчики doneCallbacks с параметрами args (обычное перечисление параметров через запятую)

deferred.reject( args )
переводит deferred объект в состояние «отменено» и вызывает все обработчики failCallbacks с параметрами args

Контекстом вызова (this внутри функции) обработчиков при использовании двух описанных выше методов будет проекция объекта deferred (или сам объект, если проекцию создать нельзя) (что такое проекция deferred см. далее в описании deferred.promise()). Если же требуется запустить функции-обработчики с другим контекстом вызова, то надо использовать следующие два метода:

deferred.resolveWith( context, args )
deferred.rejectWith( context, args )

методы полностью аналогичны .resolve( args ) и .reject( args ), только context будет доступен внутри функций обработчиков через ключевое слово this

Проверить состояние deferred объекта можно, используя методы:

deferred.isResolved()
deferred.isRejected()

методы возвращают true или false в зависимости от настоящего состояния объекта. В момент выполнения обработчиков (но пока ещё не выполнен последний) соответствующий метод вернёт true.

Создание проекции методом deferred.promise()


Этот метод создаёт и возвращает «проекцию» deferred объекта — это своебразная копия объекта, у которой есть методы добавления обработчиков и проверки состояния. Т.е. мы получаем deferred объект, который работает с той же очередью обработчиков, что и оригинал, позволяет добавлять в неё обработчики, просматривать состояние оригинального объекта, но не даёт возможности изменить состояние оригинального объекта.

Создание проекции необходимо, когда нужно выдать внешнему коду возможность добавлять обработчики в очередь и при этом защититься от несанкционированного вызова методов изменения состояния объекта.

Создание Deferred Object


Создать deferred объект можно двумя путями: создать новый собственный экземпляр методом $.Deferred() (ключевое слово new не обязательно) или вызвать метод $.when( args ), чтобы использовать уже созданные ранее deferred объекты.

$.Deferred( func )

Метод возвращает новый deferred объект. Аргумент func не обязательный, в нём можно передать функцию, которая проинициализирует созданный объект перед его возвратом из конструктора. Внутри функции func обратиться к инициализируемому deferred объекту можно через this и/или через первый аргумент фунции.

$.when( deferred1, deferred2,… )

Этот метод применяется для создания deferred объекта, который перейдёт в состояние «выполнено» после того, как все его аргументы (deferred объекты) перейдут в это состояние, либо перейдёт в состояние «отменено», как только хотя бы один из его аргументов будет отменён.

Если требуется дождаться выполнения всех задач и только потом выполнить наши задачи, то в таком случае самое место этому методу.
Пример 1.
$.when($.get('/echo/html/'), $.post('/echo/json/')).done(function(args1, args2) { alert("загрузка завершена"); });

Этот код выведет сообщение «загрузка завершена» только после того, как обе страницы будут успешно загружены в указанные блоки.

Если в качестве аргумента $.when() попадается не deferred объект, то считается, что эта задача уже успешно завершена и программа метода ориентируется на остальные аргументы.

При переходе deferred объекта, полученного методом $.when(), в состояние «выполнено» контекстом вызова (this внутри функции) обработчиков будет проекция этого deferred объекта, а аргументами в обработчики будут переданы массивы, в которых будут находится соответствующие аргументы выполнения переданных в $.when() deferred объектов (или один сам аргумент, если он один). В приведенном выше примере args1 — массив аргументов вызова success для первого ajax-запроса, а args2 — для второго.

Использование Deferred Object в $.ajax()


В jQuery 1.5 движок ajax был кардинально переписан и теперь метод $.ajax() и его сокращённые версии тоже используют deferred объекты. Это даёт возможность назначать довольно простым способом несколько обработчиков на события завершения запроса (успешного или ошибкой), а также назначать обработчики завершения нескольких параллельных запросов.

Из методов $.ajax(), $.get(), $.post(), $.getScript(), $.getJSON() возвращается объект, который обладает проекцией deferred объекта, т.е. у него есть методы, которые позволяют добавить обработчики в очередь и использовать этот объект в качестве аргумента $.when().

Дополнительно к этому объекту добавлены синонимы методов, которые по именам более подходят к теме ajax-запросов:
  • deferred.success = deferred.done — добавить обработчик успешного завершения запроса
  • deferred.error = deferred.fail — добавить обработчик завершения запроса ошибкой
  • deferred.complete — добавить обработчик завершения запроса (не важно, успешного или ошибкой), вызывается уже после всех обработчиков, добавленных через success/done или error/fail

Интересные факты


Метод $.when(), когда оценивает свои аргументы на принадлежность их к deferred объектам, проверяет наличие у них метода .promise(), и если такой метод есть, то даже «левый» объект считается deferred. С этим связан баг, когда обращение к таким «левым» объектам, будто они есть deferred, приводит к ошибкам исполнения.

В последних разрабатываемых версиях jQuery (jquery-git.js) у deferred объекта доступен ещё метод .invert(), который, аналогично методу .promise(), создаёт проекцию объекта только с методами «наоборот»: done <=> fail, isResolved <=> isRejected, promise <=> invert. Применение этого метода пока мной не замечено.

Примеры использования


В примере 1. (см. выше) уже показано решение типовой проблемы отслеживания окончания выполнения нескольких ajax запросов.

Пример 2.
// запуск задач через 3 секунды
function test() {
  var d = $.Deferred();
  setTimeout(function() { d.resolve(); }, 3000);
  return d.promise();
}

var t = test().done(function() { alert("время истекло"); });

// пытаемся добавить задачу уже после выполнения
setTimeout(function() {
  alert("добавляем задачу поздно");
  t.done(function() { alert("выполнено"); });
}, 5000);


* This source code was highlighted with Source Code Highlighter.

Это чисто демонстрационный пример, конструируется deferred объект, состояние которого переводится в «выполнено» через 3 секунды, и выдается внешнему коду. Во внешнем коде один обработчик добавляется сразу же — он вызывается по истечении 3 секунд, а второй спустя 5 секунд после конструирования, когда состояние уже «выполнено», поэтому он вызывается сразу же при добавлении.

Пример 3.
<div id="d1" style="width:100px;height:100px;background:red;">1</div>
<div id="d2" style="width:100px;height:100px;background:green;">2</div>


* This source code was highlighted with Source Code Highlighter.

// нужно дождаться конца всей анимации
var a1 = $.Deferred(),
  a2 = $.Deferred();

$('#d1').slideUp(2000, a1.resolve);
$('#d2').fadeOut(4000, a2.resolve);

a1.done(function() { alert('a1'); });
a2.done(function() { alert('a2'); });
$.when(a1, a2).done(function() { alert('both'); });


* This source code was highlighted with Source Code Highlighter.

Этим кодом представлен пример решения уже более реальной проблемы, когда нужно по завершении нескольких анимаций выполнить какой-нибудь код. Добавить обработчик завершения анимации на одном элементе достаточно просто (это тоже используется), а вот отследить завершение независимых анимаций несколько сложнее. С применением deferred объектов и $.when() код становится простым понятным.

Все примеры можно запустить и проверить на сервисе jsfiddle.net

Материалы:
blog.jquery.com/2011/01/31/jquery-15-released
api.jquery.com/category/deferred-object
www.erichynds.com/jquery/using-deferreds-in-jquery ( перевод: habrahabr.ru/blogs/jquery/112960 )
Исходный код и документация jQuery 1.5
Зайцев Андрей @zandroid
карма
57,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (16)

  • +2
    Отличная статья, теперь понятно где применить.
  • +1
    в конце $.wen() — кажется ошибочка закралась
    • +1
      Спасибо тем, кто заметил и сообщил, — исправил.
  • 0
    Наконец-то они это сделали и, за счёт популярности, унифицировали-стандартизировали.
    Больше не придётся изобретать велосипеды :)
  • 0
    смахивает на питонячий Twisted. Не просто смахивает, а один к одному относится. Насколько понимаю, концепцию оттуда и позаимствовали?
    • 0
      jQuery Deferred is based on the CommonJS Promises/A design. источник
      • 0
        ну это-то да, это — славно и все равно.

        Только проект CommonJS начался в начале 2009 года, а Twisted — в 2002. Интересно все же, где именно родилась концепция цепочек callback и errback.
  • –7
    кто не положил в закладки — тот нуб
  • 0
    Хм, не могу найти цепочек вызовов
    • 0
      Цепочки вызова выглядят, примерно, так: $.ajax(options).done(function() { }).done(function() { }).fail(function() { });

      Это как при работе с DOM элементами: $('div').attr('title', 'test').css('color', 'red').text('123');
      • 0
        Это да, но я про другие цепочки. Хотелось бы вдобавок видеть что-то вида:

        ajax1 -> (ajax2, fadeOut, static1) -> ajax3 -> (static2, ajax4) ->…

        Т.е. выстраивать такие вот очереди (тем же .then, без помощи $.when). По-идее это должно входить в Deferred. А сейчас оно получается так же неудобно, как и раньше.
        • 0
          А синтаксически корректный пример, как с then это может быть удобнее, можно привести?
          • 0
            Что-то вроде:
            $.get('/ajax/1/') 
            .done(function(){
                console.log('ajax 1 success'); // пока мы в контексте первого ajax-вызова
            })
            .then(function(){
                return $.get('/ajax/2/'); // но как только один из слушателей возвращает promise ...
            })
            .done(function(){
                console.log('ajax 2 success'); // ... контекст меняется
            })
            


            Как-то так. О чем-то таком уже писали на Хабре и вроде как оно реализовано в dojo.Deferred объекте. Вот мне и интересно, есть ли такое в jQuery?

            Возможно, разработчики jQuery просто не успели это сделать к выходу. Или же просто я этого не нашёл
            • 0
              … или ввести бы для $.Deferred свой метод .when:
              $.get('/ajax/1/')
              .done(...)
              .fail(...)
              .when(
                  function(){
                      return $.get('/ajax/2/');
                  },
                  function(){
                      var d = $.Deferred();
                      $('#id').fadeOut(d.resolve);
                      return d.promise();
                  }
              )
              .done(...)
              .fail(...)
              
              • 0
                Интересное предложение. Да, в jquery.Deferred сейчас возврат из «слушателей» никак не учитывается. Думаю, разработчики сделали выбор в пользу простоты реализации, опустив некоторые фичи.

                Надо будет на досуге подумать, что тут можно сделать, кроме вложенных функций.
              • 0
                Можно порадоваться, в версии 1.6 сделали почти то, что ты и просил: habrahabr.ru/blogs/jquery/118713/#deferredpipe

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