Pull to refresh

Использование Deferred объектов в jQuery 1.5

Reading time6 min
Views37K
Original author: Eric Hynds
Deferred объекты появились в jQuery 1.5. Они позволяют отделить логику, которая зависит от результатов выполнения действия от самого действия. Для JavaScript Deferred объекты не новы, они уже были в MochiKit и Dojo, но с изменениями логики jQuery ajax от Julian Aubourg, внедрение Deferred объектов было неминуемо. С Deferred объектами несколько callback могут быть связаны с результатом задачи и любые из них могут быть привязаны к действию даже после начала его выполнения. Выполняемая задача может быть асинхронна, но не обязательно.

Deferred объекты теперь встроены в $.ajax() таким образом вы будете получать их автоматически. Обработчики теперь могут быть связаны с результатом следующим образом:
// $.get, ajax запрос, он асинхронный по умолчанию
var req = $.get('foo.htm')
   .success(function( response ){
      // что-нибудь делаем с ответом
   })
   .error(function(){
      // делаем что-нибудь если запрос провалился
   });
 
// это выполнится перед тем как $.get() будет выполнено
doSomethingAwesome();
 
// Делаем что-то ещё перед завершением запроса
req.success(function( response ){
   // делаем  что-то ещё с ответом
   // он будет выполнен когда запрос завершится, а если запрос завершен, то будет вызван немедленно
   // если запрос уже был выполнен
});


Мы теперь не ограничены одним обработчиком success, error или complete, теперь у нас есть самоорганизуемые очереди обратных вызовов(FIFO), взамен простых функций обратного вызова.

Как показано в приведенном выше примере, обратные вызовы могут быть прицеплены даже после AJAX запроса или либо после выполнения любой задачи. Это великолепно для организации кода, дни длинных блоков обратных вызовов сочтены.

Копнем глубже, представьте себе ситуацию, в которой мы хотим вызвать функцию после нескольких одновременных запросов AJAX. Это легко сделать с хэлпером отложенных объектов — $.when():
function doAjax(){
   return $.get('foo.htm');
}
 
function doMoreAjax(){
   return $.get('bar.htm');
}
 
$.when( doAjax(), doMoreAjax() )
   .then(function(){
      console.log( 'I fire once BOTH ajax requests have completed!' );
   })
   .fail(function(){
      console.log( 'I fire if one or more requests failed.' );
   });

Пример на jsFiddle jsfiddle.net/ehynds/Mrqf8

Это все работает потому что сейчас все методы jQuery AJAX возвращают объект, содержащий Promise объект, который используется для отслеживания асинхронных вызовов. Promise это доступный на чтение наблюдатель за результатом действия. Deferred объекты смотрят на наличие метода promise() для определения возможно ли наблюдать за объектом или нет (object is observable or not). $.when() ждет когда все AJAX запросы будут выполнены, когда это произойдет функции обратного вызова, прицепленные к $.when() через .then() и .fail(), будут вызваны в зависимости от того с каким результатом завершилась задача. Функции обратного вызова вызываются в порядке поступления(fifo очередь).

Все отложенные методы (.then() и .fail()) принимают функции или массивы функций, так что вы можете создать свои поведения и назначить их все с помощью одного вызова или разделить их, как вам будет удобно.

$.ajax() возвращает не стандартный объект, содержащий другой Deferred-подобный объект. Я уже описал promise(), но вы также нашли then(), success(), error() и множество других. У вас нет доступа ко всему Ajax-Deferred объекту. Только promise(), методы обратного вызова, а так же методы isRejected() и isResolved(), которые могут быть использованы для проверки статуса Deferred объекта.

Но почему бы не вернуть весь объект? Если это было так, то можно было бы программно завершить Ajax-Deferred объект, не дожидаясь Ajax ответа. Это потенциально нарушает всю парадигму.

Регистрация Callback'ов


В примерах, показанных выше, методы then(), success() и fail() используются для регистрации функций обратного вызова в Deferred объектах, но вам доступно больше методов, особенно когда вы работаете с Ajax-Deferred.

Методы доступны всем Deferred объектам(AJAX, $.when и те, которые создаются руками):
.then( doneCallbacks, failedCallbacks )
.done( doneCallbacks )
.fail( failCallbacks )

Ajax-Deferred объект имеет 3 дополнительных метода, которые просто дублируют описанные выше. Они предоставляют из себя семантические альтернативы и схожи по именам со старыми обработчиками, к которым мы все привыкли:
.complete( doneCallbacks, failCallbacks )
.success( doneCallbacks )
.error( failCallbacks )

Таким образом, следующие три примера эквивалентны (success читается лучше, чем если бы он был в контексте запроса AJAX, не правда ли?)
$.get("/foo/").done( fn );
 
// аналогично:
 
$.get("/foo/").success( fn );
 
// аналогично:
 
$.get("/foo/", fn );


Создание своего Deferred поведения


Мы знаем, что методы $.ajax и $.when предоставляют нам deferred API внутри, но мы можете сделать свой собственный вариант:
function getData(){
   return $.get('/foo/');
}
 
function showDiv(){
    var dfd = $.Deferred();
 
    $('#foo').fadeIn( 1000, dfd.resolve );
 
    return dfd.promise();
}
 
$.when( getData(), showDiv() )
    .then(function( ajaxResult ){
        console.log('The animation AND the AJAX request are both done!');
 
        // 'ajaxResult' это ответ от сервера
    });

Пример на jsFiddle jsfiddle.net/ehynds/JSw5y

Внутри showDiv() я создаю новый Deferred объект, выполняющий анимацию и возвращающий promise. Deferred завершается (вспомните dequeue(), если вы знакомы с методами работы с очередями в jQuery) после того как fadeIn() завершится. Между моментом времени когда возвращается promise и завершается Deferred, мы регистрируем callback, ожидающий удачное выполнение обоих задач. Поэтому, когда оба задания завершены, callback будет выполнен.

getData() возвращает объект с методом promise, который позволяет $.when() наблюдать за выполнением действия. Подобный объект возвращает showDiv()

Отложенные Deferred объекты


Мы можем сделать ещё один шаг для регистрации индивидуальных функций обратного вызова у getData() и showDiv() и регистрации их promise'ов в одном мастер-Deferred объекте.

Если вы желаете, чтобы что-нибудь произошло после выполнения success getData() или success showDiv() независимо, так же как и после success обоих getData() и showDiv(), просто зарегистрируйте callback для их индивидуальных Deferred объектов и свяжите их вместе через $.when
function getData(){
   return $.get('/foo/').success(function(){
      console.log('Fires after the AJAX request succeeds');
   });
}
 
function showDiv(){
    var dfd = $.Deferred();
 
    dfd.done(function(){
      console.log('Fires after the animation succeeds');
    });
 
    $('#foo').fadeIn( 1000, dfd.resolve );
 
    return dfd.promise();
}
 
$.when( getData(), showDiv() )
    .then(function( ajaxResult ){
        console.log('Fires after BOTH showDiv() AND the AJAX request succeed!');
 
        // 'ajaxResult' is the server’s response
    });

Пример на jsFiddle jsfiddle.net/ehynds/W3cQc

Цепочки Deferred


Обратные вызовы Deferred могут быть представлены в виде цепочек, так как promise возвращается из callback методов. Вот реальный пример от @ajpiano:
function saveContact( row ){
   var form = $.tmpl(templates["contact-form"]),
      valid = true,
      messages = [],
      dfd = $.Deferred();
 
   /*
      bunch of client-side validation here
   */
 
   if( !valid ){
      dfd.resolve({
         success: false,
         errors: messages
      });
   } else {
      form.ajaxSubmit({
         dataType: "json",
         success: dfd.resolve,
         error: dfd.reject
      });
   }
 
   return dfd.promise();
};
 
saveContact( row )
   .then(function(response){
      if( response.success ){
         // saving worked; rejoice
      } else {
         // client-side validation failed
         // output the contents of response.errors
      }
   })
   .fail(function(err) {
      // AJAX request failed
   });

Функция saveContact() проверяет форму и сохраняет результат в переменную valid. Если валидация провалилась, deferred объект завершается с объектом, содержащим булеан success и массив ошибок. Если форма прошла валидацию — deferred объект завершается, но в этот раз с ответом от AJAX запроса. Обработчик fail() слушает ошибки транспорта (404, 500 и другие)

Не наблюдаемые задачи


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

В этом примере, когда первый раз кликаем на ссылку «launch application», AJAX запрос говорит серверу сохранить и возвратить текущий timestamp. Timestamp сохраняется в data-кэше элемента после выполнения AJAX запроса. Приложение заботится только о первом клике. На последующих клика timestamp берется из data-кэша, вместо того, чтобы делать запрос на сервер.
function startTask( element ){
   var timestamp = $.data( element, 'timestamp' );
 
   if( timestamp ){
       return timestamp;
   } else {
       return $.get('/start-task/').success(function( timestamp ){
           $.data( element, 'timestamp', timestamp );
       });
   }
}
 
$('#launchApplication').bind('click', function( event ){
   event.preventDefault();
 
   $.when( startTask(this) ).done(function( timestamp ){
       $('#status').html( '<p>You first started this task on: ' + timestamp + '</p>');
   });
 
   loadApplication();
});


Когда $.when() понимает, что его первый аргумент не имеет promise (и поэтому он не наблюдаемый), он создает новый deferred объект и завершает его с объектом data и возвращает promise этого отложенного объекта. Так, что любой объект без promise может быть наблюдаемым.

Одно маленькое но, вы не можете сделать отложенный запуск объекта, который возвращает объект со своим методом promise. Deferred объекты определяются о наличию метода promise, но jQuery не проверяет возвращает ли promise необходимый объект. Поэтому этот код содержит ошибку syntax error:
var obj = {
   promise: function(){
      // do something
   }
};
 
$.when( obj ).then( fn );


Заключение


Deferred объекты вводят новый надежный способ для написания асинхронных задач. Вместо того чтобы, сосредоточиться на том как организовать логику обратного вызова в один callback, вы можете назначить несколько отдельных действий в очереди обратного вызова, зная, что они будут выполнены, не так сильно беспокоясь о их синхронности. Предложенную информацию нужно долго переваривать, но когда вы все усвоите, то я думаю вы поймете, что асинхронный код намного проще с Deferred объектами.
Tags:
Hubs:
+110
Comments19

Articles

Change theme settings