Il fattore che più di tutti ha inciso sul successo di JavaScript è la natura asincrona che possiamo far assumere a molte operazioni. Grazie a metodologie di sviluppo come AJAX, infatti, è possibile velocizzare e rendere più reattive le applicazioni lato client eliminando colli di bottiglia o suddividendo operazioni molto complesse in piccoli task.
Introduzione a Promises e Deferred operations
Già da qualche tempo alcuni framework hanno esteso la possibilità di creare operazioni asincrone oltre le semplici chiamate AJAX, implementando il concetto di Promises o esecuzione differita.
Quello che avviene è che una funzione, invece di bloccare l'esecuzione dell'applicazione in attesa di un valore, restituisce una promessa che nel futuro quel valore sarà disponibile. Una volta che il dato è arrivato, la promessa viene considerata come mantenuta e le operazioni dipendenti da quel valore possono essere eseguite.
Lo scenario tipico di utilizzo di un sistema a promesse è quello di un'applicazione dipendente da servizi esterni (ad esempio Google Maps) che, a causa di rallentamenti, possono bloccare l'esecuzione di tutto lo script.
Nella sua stesura originaria, e nella maggior parte delle implementazioni attuali, la struttura base di una promise è la seguente:
promessa = PromiseFunction( ... );
promessa.then(function () {
/*
codice da eseguire
quando la promessa è
mantenuta
*/
});
A livello sintattico le promesse permettono di semplificare la stesura del codice e renderla meno nidificata:
promessa.then(function () {
/* ... */
});
/* ... altro codice ... */
promessa.then(function () {
/*
accodo una nuova funzione
*/
});
In molti casi è anche possibile stabilire quando la promessa non possa considerarsi mantenuta ed aggiungere una seconda callback che gestisca l'errore:
promessa.then(
function () {
alert('promessa mantenuta');
},
function () {
alert('la promessa non è stata mantenuta');
}
);
Un ultimo caso che potrebbe presentarsi in certe applicazioni, è quello in cui sia necessario concludere più promesse
when
when(
promessa1,
promessa2
).then(function () {
...
});
Introduzione a jQuery $.Deferred
Sebbene framework come Dojo abbiano implementato ormai da tempo il concetto di promise, jQuery lo ha introdotto solo a partire dalla versione 1.5 attraverso il metodo $.Deferred()
, che a sua volta implementa i metodi visti in precedenza ed alcune altre utility:
//questo oggetto non ha promesse
//ma è un contenitore vuoto
var deferred = $.Deferred();
deferred.then(
function () {
alert('promessa mantenuta');
},
function () {
alert('promessa NON mantenuta');
}
);
//la promessa è impostata come
//mantenuta
deferred.resolve();
Chiamate asincrone
La mossa intelligente, in linea con la filosofia jQuery è, stata quella di riscrivere i metodi per le chiamate asincrone ($.ajax()
e derivati) in modo che restituiscano un oggetto compatibile con $.Deferred
(più specificatamente una promessa) al posto dell'oggetto nativo XMLHttpRequest
. Anzitutto, questa novità permette di associare le callback della chiamata direttamente come metodi dell'oggetto, piuttosto che passandole come parametri (opzione che comunque resta disponibile):
//vecchia sintassi
$.get('pagina.php', function (data) {
// ...
});
//da jQuery 1.5
$.get('pagina.php')
.success(function (data) {
// ...
});
Inoltre, è anche possibile concatenare più metodi all'oggetto, oppure associare altre callback durante l'esecuzione dello script:
var jqxhr = $.get('pagina.php');
jqxhr
.success(function () {
// ...
})
.error(function () {
// in caso di errore ...
});
//codice indipendente dalla richiesta
$('#mioDiv').addClass('visibile');
jqxhr.always(function () {
// funzione da eseguire sempre
// alla fine della richiesta
});
Il particolare da notare è che, per la natura degli oggetti deferred, se viene accodata una funzione dopo che la chiamata è conclusa, essa verrà comunque valutata.
Nell'esempio precedente, se la chiamata a pagina.php
si conclude prima che venga accodata la funzione .always()
, quest'ultima verrà lanciata immediatamente.
Il metodo $.when
Nonostante buona parte delle operazioni asincrone in un'applicazione siano rappresentate da semplici chiamate AJAX, vi sono casi in cui ricorrere alle promise può semplificare il codice e rendere meno dipendenti le varie logiche dello script.
In questi scenari possiamo rivolgerci al metodo $.when()
che accetta come argomenti dei riferimenti a promesse ed è concatenabile con tutti i metodi di $.Deferred
.
Un primo esempio potrebbe essere quello già introdotto di chiamate multiple che mantengono un'unica promessa:
function getPagina(url) {
return $.ajax(url, {dataType : 'text'});
}
$.when(
getPagina('pagina.html') ,
getPagina('pagina2.html')
)
.then(function (response, response2) {
$('body').append(response[0], response2[0]);
});
Da notare che, poiché $.get()
response
response2
Ecco l'esempio in azione.
Un altro caso interessante potrebbe essere quello di usare una promessa per controllare la conclusione di una serie di animazioni:
function showMe (selector, speed) {
//un oggetto deferred custom
var dfd = $.Deferred(),
s = speed || 'fast';
//passando il metodo .resolve()
//come callback di .show()
//la promessa è mantenuta quando l'animazione è conclusa
$(selector).show(s, dfd.resolve);
//restituisco la promessa in modo che
//sia controllabile con $.when
return dfd.promise();
}
$.when(
showMe('#primoDiv'),
showMe('#secondoDiv', 'slow')
).then(function () {
alert('Tutti gli elementi sono stati mostrati');
});
Ecco una dimostrazione pratica di questo esempio
Gestire la cache con i deferred
Un particolare uso di $.when()
è quello di accettare come argomenti anche oggetti diversi dalle promesse. In tal caso il metodo interpreterà tali valori come promesse mantenute e li passerà alle eventuali funzioni di callback impostate:
var valore = 'una stringa';
$.when(valore)
.then(function (result) {
//result === 'una stringa'
});
Questo comportamento permette di utilizzare $.when()
Un esempio abbastanza comune è quello del caricamento asincrono di template JavaScript:
vvar templateCache = null;
function getTemplate () {
if (templateCache !== null) {
//se il template è già
//caricato lo restituisco
return templateCache
} else {
//altrimenti lo carico
//e restituisco una promessa
alert('caricamento via AJAX...');
return $.ajax('template.js', {dataType : 'text'});
}
}
function addName () {
//estraggo i dati da campi input
var nome = $('#nome').val() || '',
cognome = $('#cognome').val() || '';
$.when( getTemplate() )
.then(function (template) {
//se il template non è in cache lo imposto
if (templateCache === null) {
templateCache = template;
}
$.tmpl(template, { nome : nome, cognome : cognome})
.appendTo('body');
});
}
Ecco un esempio
Risorse a approfondimenti
Poiché le possibili implementazioni e gli scenari di utilizzo dei deferred vanno ben oltre questa breve introduzione, ecco una lista di risorse ed approfondimenti sulla materia: