Categories: jquerytodas

O objeto Deferred da jQuery 1.5

Nota inicial: A recém lançada versão 1.5 da biblioteca jQuery trouxe como sua maior novidade a criação do objeto Deferred para requisições AJAX. Trata-se de um objeto contendo vários métodos que visam, em última análise, a permitir que se atrele múltiplos callbacks aos diversos eventos que ocorrem ao curso de uma requisição sem preocupações com sincronismo.
Eric Hynds um desenvolvedor front-end de Boston publicou uma máteria explicando essa nova funcionalidade que apresento aos meus leitores devidamente traduzida.

Deferred é uma funcionalidade nova da biblioteca jQuery 1.5 e destina-se a dissociar os resultados de uma requisição da própria requisição. Este é um conceito já conhecido em cenários JavaScript; as bibliotecas Mochikit e Dojo já o implementam há algum tempo, mas como analisado por Julian Aubourg’s a implementação de Deferred na jQuery foi um passo lógico e natural. Essa funcionalidade permite que se atrele callbacks múltiplos aos eventos de uma requisição e, mais ainda, qualquer um dos callbacks pode ser atrelado quando a requisição se completa. A requisição em questão pode ser, não necessariamente, assíncrona.

E, mais ainda, Deferred está incorporada ao método $.ajax(), ou seja, está disponível automaticamente. Manipuladores de eventos podem ser atrelados como mostrado a seguir:

//
//
// $.get, é uma requisição AJAX assíncrona por padrão.  
var req = $.get('foo.htm').success(function( response ){        
// fazer algo com response     
}).error(function(){        
// fazer algo se a requisição falhar     
});     
// código a ser executado antes que $.get() tenha terminado  
doSomethingAwesome();     
// código a ser executado no sucesso da requisição, podendo ou não
// ser disparado já.  como Deferred está incorporado a $.ajax isso não faz diferença.  
req.success(function( response ){     
// mais código a ser executado no sucesso da requisição      
// dispara normalmente com o disparo de success ou imediatamente     
// caso o callback anterior de success já tenha disparado.  
});

Agora não estamos mais limitados a um único manipulador para cada evento: sucesso, error, ou tarefa completada. Em lugar de funções callback únicas agora é possível criar uma fila de callbacks auto gerenciada.

Como mostrado no exemplo anterior agora é possível atrelar callbacks depois que a requisição AJAX se completou ou depois que um evento de observação (sucesso, erro) tenha se completado. Isso é muito bom para a organização do código; os dias de desenvolvimento de callbacks intrincados terminaram. Funciona mais ou menos como se fosse possível organizar uma fila de callbacks.

Suponha um cenário no qual desejamos chamar uma função logo após várias requisições AJAX concorrente terem sido completadas. Isso é possível com o uso do método $.when() de Deferred como mostrado a seguir:

function doAjax(){     
return $.get('foo.htm');  
}     
function doMoreAjax(){     
return $.get('bar.htm');  
}     
$.when( doAjax(), doMoreAjax() ).then(function(){        
console.log( 'Disparei assim que AMBAS as requisições se completaram' );     
}).fail(function(){        
console.log( 'Disparei se uma ou mais requisições falharam' );     
});

Ver esse exemplo no jsFiddle

Isso funciona porque agora todos os métodos AJAX da jQuery retornam um objeto contendo uma "promise", que é usada para monitorar o andamento da requisição assíncrona. A "promise" é uma saída do tipo somente leitura para o andamento da requisição. Deferred usa o método promise() para determinar se um objeto da requisição é observável ou não. O método $.when() espera pela execução das requisições AJAX e quando terminadas os callbacks a ele atrelados com uso dos métodos .then() e .fail() são devidamente disparados (nos casos de sucesso ou falha). Os callbacks disparam na ordem em que foram definidos.

E as vantagens não param aí: todos os métodos de Deferred aceitam funções ou arrays de funções. Isso permite que se projete comportamentos a serem disparados com uma chamada ou várias chamadas, conforme nossas necessidades.

$.ajax() retorna um objeto contendo um pacote de métodos de Deferred. Eu citei promise(), mas existem ainda os métodos then(), success(), error() e muitos outros. Você não tem acesso ao pacote completo de objetos Deferred, mas somente aos métodos de atrelamento para promise() e aos métodos isRejected() e isResolved(), que são usados para verificar o estado de Deferred.

Mas, por que não temos acesso à todos os objetos? Se tivéssemos acesso seria possível ir além talvez até pragmaticamente quot;resolve" o Deferred, fazendo callbacks serem executados antes que a requisição AJAX se completasse. Isso contraria um paradigma.

Atrelando callbacks

Nos exemplos anteriores eu usei os métodos then(), success(), e fail() para atrelar callbacks com Deferred, mas existem outros métodos disponíveis especialmente quando se trabalha com AJAX Deferred. O método a usar depende do estado ao qual o callback deva ser atrelado.

Disponíveis para todos os Deferred (AJAX, $.when, e os criados manualmente):

.then( doneCallbacks, failedCallbacks )  
.done( doneCallbacks )  
.fail( failCallbacks )

AJAX Deferred têm três métodos adicionais que mapeiam para um dos métodos mostrados anteriomente. Eles foram disponibilizados como alternativas semanticas para métodos já existentes

//
//
// "success" e "error" mapeiam para "done" e "fail" respectivamente.  
.success( doneCallbacks )  
.error( failCallbacks )

Você pode atrelar o manipulador complete que dispara independentemente de a requisição ser bem sucedida ou falhar. Diferente de success e error complete é um alias para done em um objeto Deferrred separado. Esse objeto Deferred separado é criado internamente por $.ajax() e resolvido tão logo a requisição se complete independentemente do seu resultado.

.complete( completeCallbacks )  

Assim, os três exemplos mostrados a seguir são equivalentes (A palavra success (sucesso) é muito mais amigável que done (feito) no contexto de AJAX. Você não acha?)

$.get("/foo/").done( fn );     
// é o mesmo que:     
$.get("/foo/").success( fn );     
// é o mesmo que:     
$.get("/foo/", fn );

Criando seus Deferred

Sabemos que $.ajax e $.when implementam suas API Deferred internamente, mas você pode criar suas implementações personalizadas:

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('A animação e a requisição AJAX, ambas terminaram');             
// 'ajaxResult' é a resposta do servidor      
});

Ver esse exemplo no jsFiddle

Na função showDiv() estou criando um novo objeto Deferred com a finalidade de produzir uma animação é retornar promise. O Deferred se resolve (pense em dequeue() se você conhece os métodos de enfileiramento da jQuery) após o completamento de uma chamada para fadeIn(). Entre o tempo de retorno de promise e a resolução de Deferred um callback com uso do método then() é atrelado ao sucesso das duas requisições assíncronas. Assim, quando ambas as requisições se completam o callback é disparado.

getData() retorna um método promise() que permite a $.when() observar sua resolução. Os passos que criamos manualmente para retornar promise na função showDiv() são manipulados internamente por $.ajax() e $.when().

Adiando Deferred

Podemos ir além do que foi mostrado no exemplo anterior atrelando callbacks individuais para getData() e showDiv(), bem como definindo suas promises individuais dentro de um "master" Deferred.

Se você pretende que algo seja executado quando do sucesso de getData() e de showDiv() (independentemente um do outro), e também que algo seja executado quando do sucesso de ambos getData() e showDiv() combinados, simplesmente atrele callbacks aos seus Deferred individuais e junte-os com uso de $.when:

function getData(){     
return $.get('/foo/').success(function(){        
console.log('Dispara depois do sucesso da requisição AJAX');     
});  
}     
function showDiv(){      
var dfd = $.Deferred();         
dfd.done(function(){        
console.log('Dispara depois do sucesso da animação');      
});         
$('#foo').fadeIn( 1000, dfd.resolve );         
return dfd.promise();  
}     
$.when( getData(), showDiv() ).then(function( ajaxResult ){          
console.log('Dispara depois do sucesso de AMBAS as requisições showDiv() e AJAX');             
// 'ajaxResult' é a resposta do servidor      
});

Ver esse exemplo no jsFiddle

Encadeamento

Callbacks Deferred podem ser encadeados uma vez que promise seja retornada da função. A seguir mostramos um exemplo real (via @ajpiano!)

function saveContact( row ){     
var form = $.tmpl(templates["contact-form"]),        
valid = true,        
messages = [],        
dfd = $.Deferred();        
/* validação client-side */        
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 ){           
// salvar; 
rejoice        
} else {           
// validação falha           
// mostrar saída de response.errors        
}}).fail(function(err) {        
// requisição AJAX falhou     
});

A função saveContact() valida o formulário e salva o resultado na variável valid. Se a validação falha Deferred se resolve com um objeto contendo o valor booleano false para success e uma mensagem criada com um array de erros. Se a validação passa Deferred se resolve com uma resposta de sucesso da requisição AJAX. O manipulador fail() responde com erros 404, 500, e outros erros HTTP que possam impedir o sucesso da requisição AJAX.

Tarefas não observáveis

Deferred são particularmente úteis quando a lógica de execução é ou não assíncrona é a você interessa não considerar essa condição no código. Sua tarefa pode retornar não apenas promise, mas também string, objeto ou outro tipo qualquer.

No exemplo a seguir na primeira vez que a "aplicação roda" e um link é clicado uma requisição AJAX grava no servidor e retorna o timestamp atual. O timestamp é gravado em cache tão logo a requisição se completa. A aplicação considerada somente a primeira vez que o link é clicado e nos cliks subsequentes o timestamp é lido do cache em lugar de ser feita uma nova requisição ao servidor.

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>Você começou essa tarefa em: ' + timestamp + '</p>');     });        
loadApplication();  
});

Quando $.when() que seu primeiro argumento não tem um promise (e em consequência não é observável), ele cria um novo objeto Deferred resolve ele com uso de data e retorna o método promise() de Deferred.

Um possível bug, que deverá ser corrigido na próxima versão da biblioteca e o fato de que você não pode adiar um objeto que implementa seu próprio método promisse(). Deferred são detectados pela presença do método promise(), mas jQuery não faz nenhuma verificação se o objeto retornado por promisse() é usável. Assim o exemplo a seguir acusa um erro de sintaxe:

var obj = {     promise: function(){        
// faça algo     
}};     
$.when( obj ).then( fn );

Conclusão

Deferred introduz uma nova e robusta maneira de se projetar requisições assíncronas. Em lugar de se preocupar em como orgaanizar a lógica de callbacks em um único callback você agora pode definir várias ações individuais de callback em fila e estar seguro que elas serão executadas no contexto em que foram inseridas sem se preocupar com sincronismo. O assunto abordado nessa matéria está longe de ter esgotado todas a possibilidades dessa nova funcionalidade da biblioteca jQuery, mas uma vez que você tenha entendido como ela funciona estará apto a escrever código assíncrono com muito maior simplicidade.

Muito obrigado a Steven Black pela edição dessa matéria. Obrigado ao pessoal da #jquery-dev por ter me ajudado a entender Deferred. E obrigado a Adam que criou um belo exemplo sobre Deferred.

Nota em 23/01/2012: O William Bruno publicou uma matéria contendo exemplos práticos do uso do objeto Deferred em Exemplo de uso jQuery.Deferred – simples.

Maujor

View Comments

Share
Published by
Maujor

Recent Posts

Teste seu conhecimento #20

Em 2006 comecei a publicar nesse blog uma série de desafios CSS que consistiam em…

7 anos ago

Teste seu conhecimento #19

Há muito tempo que eu não publico um "Teste seu conhecimento". Esta semana, revendo algumas…

9 anos ago

JavaScript bubbling e capturing

Introdução Elementos da marcação HTML podem ser aninhados uns dentro de outros, criando-se uma cadeia…

10 anos ago

HTML5 – W3C versus WHATWG

HTML5? Web universal? É comum eu me deparar com dúvidas sobre a HTML5 não só…

10 anos ago

Seria esse o futuro das imagens responsivas?

Quem é Tab Atkins Jr? Tab Atkins Jr, um desenvolvedor americano, trabalhou durante muitos anos…

10 anos ago

BrazilJS Conf 2013

Pessoal, a BrazilJS Conf 2013 disponibilizou para o Maujor cupons de desconto para serem oferecidos…

11 anos ago