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 per mantenere un impegno (ad esempio raccogliere dati da fonti diverse). In questo scenario può essere utile utilizzare il metodo 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()
restituisce più di un valore (nello specifico risposta, responseText e oggetto AJAX), i due argomenti response
e response2
sono passati come array. In questo modo nessun dato restituito dopo la risoluzione delle promesse viene perso.
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()
in tutti quei casi in cui i dati non sono subito disponibili, ma vengono caricati via AJAX per poi essere salvati in una cache interna.
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: