Javascript si sta facendo sempre più prominente non solo sul web ma anche sul desktop e sui server. Le ultime versioni del linguaggio offrono diverse migliorie, specie nel campo della programmazione asincrona.

Javascript è fondamentalmente mono-thread, cioè gli script vengono eseguiti in un unico processo. Per essere utilizzabile in una pagina web, dove l'utente interagisce con l'interfaccia in maniera non lineare, viene usato un sistema di eventi e di "callback", e l'accesso alle risorse avviene in maniera non bloccante.

Fondamentalmente, in javascript si definisce del codice che verrà eseguito nel momento in cui si verifica un evento, sia esso un click su un bottone, lo scadere di un timer, dei dati in arrivo di ritorno da una richiesta http, etc.

Un esempio veloce:

<!DOCTYPE html>
<button id="bottone">cliccami</button>

<script>
function bottone_cliccato(){
    console.log("ouch!");
}

var btn = document.getElementById("bottone")
btn.addEventListener('click', bottone_cliccato);
console.log("fine script");
</script>

Abbiamo definito una funzione, bottone_cliccato, che viene chiamata quando l'utente clicca sul bottone, ovverto quando l'elemento con id bottone emette l'evento 'click'.

(live)

Lo scenario

Per questo articolo lavoreremo sul seguente scenario: quando l'utente clicca sul bottone, dobbiamo recuperare dal server dei dati, ma prima dobbiamo chiedere all'utente un "codice".

La pagina sarà la seguente:

<!DOCTYPE html>
<button id="bottone">cliccami</button>
<pre id="risultato"></pre>
<script src="app.js"></script>

Separaiamo per comodità l'html dal javascript, che mettermo in un file chiamato app.js:

var btn = document.getElementById("bottone");
var res = document.getElementById("risultato");

btn.addEventListener('click', function(event){

});

Cosa accadrà al click lo defineremo a seconda delle diverse soluzioni.

XHR e prompt

La prima soluzione è usare due grandi classici: l'oggetto XHR (XML Http Request) reso famoso prima da IE e poi dall'avvento del cosìddetto "AJAX", e la funzione prompt().

Il primo permette di eseguire richieste HTTP da javascript e di recuperarne la risposta. E' possibile configurare XHR per eseguire la richiesta in maniera sincrona. Siccome la creazione e la configurazione di una richiesta con XHR è abbastanza lunga e non ci interessa in questo momento, diciamo che abbiamo scritto una funzione doXHRSync():

/**
 * Esegue una richiesta http sincrona usando XHR.
 *
 * @param url string: Url da chiamare
 * @return object: Dati della risposta
 */
function doXHRSync(url) {
 ...
 return data;
}

Non ci interessa sapere come funziona, ci interessa sapere cosa fa e come usarla.

Il secondo pezzo necessario alla nostra soluzione è la funzione prompt(), che mostra all'utente una finestra di input e ritorna i dati, sempre in maniera sincrona.

Mettendo assieme i pezzi:

btn.addEventListener('click', function(event){
    var code = prompt("Codice:");
    var data = doXHRSync("https://httpbin.org/get?code="+code);

    res.innerText = data;
}

(live)

Più facile di così!

La documentazione, però, dice che è sconsigliato usare XHR in modalità sincrona:

Starting with Gecko 30.0 (Firefox 30.0 / Thunderbird 30.0 / SeaMonkey 2.27), synchronous requests on the main thread have been deprecated due to the negative effects to the user experience.

XHR Async e prompt()

Creiamo quindi una nuova funzione fittizia. Ancora non ci interessa sapere come funziona. ma cosa fa e come usarla.

/**
 * Esegue una richiesta http sincrona usando XHR.
 *
 * @param url string: Url da chiamare
 * @param callback function(data): Funzione da chiamare quando i dati sono pronti
 */
function doXHRAsync(url, callback) {
}

Siccome ora la chiamata è asincrona, la funzione non ritorna nulla, e si aspetta come parametro un'altra funzione che verrà chiamata quando i dati sono pronti.

Ora la nostra nuova recupera_dati()

btn.addEventListener('click', function(event){
    var code = prompt("Codice");
    doXHRAsync("https://httpbin.org/get?code="+code, function(data){
        res.innerText = data;
    });
}

(live)

Il risultato è lo stesso: l'utente clicca il bottone, gli viene chiesto il codice, viene effettuata la richiesta al server, la risposta viene mostrata nella pagina.

In questo caso pero' mentre la richiesta viene inviata e si attende e si elabora la risposta, il nostro script non viene bloccato, e quindi l'interfaccia resta attiva. Pensiamo per esempio ad un'animazione da eseguire mentre la richiesta è in corso.

Ora alziamo ancora un poco la sbarra: invece di usare la funzione prompt(), che è sincrona e quindi blocca la pagina finchè non ritorna, creiamo una finestra di dialogo usando l'html.

La finestra di dialogo asincrona.

Invece di usare la funzione prompt(), che blocca l'esecuzione del javascript, creiamo una nuova funzione che aggiunge alla pagina un dialogo che chiede all'utente il codice. La funzione accetta un callback che verrà chiamato quando l'utente clicca su "Ok" nel dialogo.

<!DOCTYPE html>
<style>
#dialogo {
    padding: 2em;
    border: 1px solid black;
}
</style>
<button id="bottone">cliccami</button>
<pre id="risultato"></pre>

<script src="app.js"></script>
/**
 * mostra un dialogo all'utente chiedendo
 * di inserire un valore
 *
 * @param testo string: Testo da mostrare all'utente
 * @param callback function(string): Funzione da chiamare con il risultato
 */
function dialogo(testo, callback) {
    var eDialogo = document.createElement("div");
    eDialogo.id = "dialogo";

    var eInput = document.createElement("input");
    var eBottone = document.createElement("button");
    eBottone.innerText = "Ok";

    eBottone.addEventListener("click", function(event) {
        var valore = eInput.value;
        callback(valore);
        eDialogo.remove();
    });


    eDialogo.appendChild(eInput);
    eDialogo.appendChild(eBottone);
    document.body.append(eDialogo);
}

btn.addEventListener('click', function(event){
    dialogo("Codice", function(code) {
        doXHRAsync("https://httpbin.org/get?code="+code, function(data){
            res.innerText = data;
        });
    });
}

(live)

La nostra funzione dialogo() crea una struttura di elementi nella pagina che equivale a questo html:

<div id="dialogo">
    <input>
    <button>Ok</button>
</div>

Un gestore eventi viene aggiunto al bottone per gestire i click. Quando l'utente clicca, salva il valore inserito nell'input, chiama il callback passandogli il valore e infine rimuove il dialogo dalla pagina.

Il codice qui è ancora abbastanza semplice, ma comincia a notarsi la "piramide di callbacks": una serie di funzioni asincrone i cui callback chiamano un'altra funzione asincrona, in cascata.

Potremmo sperarare ogni calback in una sua funzione, ma in certi casi potrebbe essere peggio dal punto di vista di chi legge il codice.

Una promessa è una promessa.

Per cercare di ridare una struttura più lineare, introduciamo Promise. Una promessa è un oggetto ritornato da una funzione asincrona che non è il valore, ma la promessa che in un futuro la richiesta verrà soddisfatta (se non si verifica un errore). Quando il valore è pronto, viene chiamato un callback definito nella Promise. La cosa interessante è che nel callback possiamo ritornare una nuova promessa e quindi concatenare i callback in maniera più lineare.

Un oggetto Promise definisce due funzioni: then() e catch(). La prima prende come parametro la funzione callback da chiamare quando il valore è pronto, la seconda una funzione callback da chiamare in caso di errore.

E' da notare che catch() puo' essere definito alla fine della catena per catturare qualunque errore si verifichi durante l'esecuzione di ogni passo.

Facciamo conto di aver creato due funzioni: dialogoPromise() e doXHRPromise(), analoghe alle precedenti, ma che ritonano una promessa.

var p1 = dialogoPromise("Codice:");
p1.then(function(code) {
    p2 = doXHRPromise("https://httpbin.org/get?code="+code);
    p2.then(function(data) {
        res.innerText = data;
    });
});

Ovviamente scritto così non guadagnamo niente.

Pero' possiamo usare la catena di promesse: esempio().then(...).then(...).catch(...);

dialogoPromise("Codice:")
    .then(function(code) {
        return doXHRPromise("https://httpbin.org/get?code="+code);
    })
    .then(function(data)) {
        res.innerText = data;
    })
    .catch(function(err)) {
        alert("Errore! " + err);
    });

Ecco la nostra catena di promesse. Il codice è un pelo più leggibile.

Come creiamo una promessa? Con la classe Promise. Vediamo la funzione dialogoPromise():

function dialogoPromise(testo) {
    var eDialogo = document.createElement("div");
    eDialogo.id = "dialogo";

    var eInput = document.createElement("input");
    var eBottone = document.createElement("button");
    eBottone.innerText = "Ok";

    eDialogo.appendChild(eInput);
    eDialogo.appendChild(eBottone);
    document.body.append(eDialogo);

    var promessa =  new Promise(function(resolve){
        // quando l'utente clicca il bottone,
        // rimuoviamo il dialogo e risolviamo la promessa
        eBottone.addEventListener("click", function(event) {
            var valore = eInput.value;
            eDialogo.remove();
            resolve(valore);
        });
    });

    return promessa;
}

La nuova funzione non prende più direttamente il callback come parametro, e ritorna un'istanza della classe Promise. E' all'interno di questa classe che viene chiamato il callback definito quando la promessa viene risolta.

In questo caso non abbiamo definito la gestione di eventuali errori: si potrebbe aggiungere un bottone "annulla" e risolvere la promessa con un errore se l'utente lo clicca.

La funzione doXHRPromise(), invece, non la scriviamo, perchè già da un po' javascript supporta la funzione fetch(), che esegue una richiesta e ritorna una promessa.

Possiamo quindi scrivere la nuova versione del nostro codice così:

btn.addEventListener('click', function(event){
    dialogoPromise("Codice")
      .then(function(code) {
          return fetch("https://httpbin.org/get?code="+code);
      })
      .then(function(response){
          return response.text();
      })
      .then(function(data){
          res.innerText = data;
      })
      .catch(function(err) {
          res.innerText = "Errore: " + err;
      });
});

(live)

Notiamo che abbiamo dovuto aggiungere un passaggio: la promessa ritornata da fetch() risolve in un oggetto Response, da cui dobbiamo recuperare il testo. Ma al momento in cui chiamiamo reponse.text(), il server potrebbe non aver finito di inviarci dati. Così anche reponse.text() ritorna una promessa, che risolve quando la pagina richiesta è completamente caricata e il testo è disponibile.

Ne approfittiamo per gestire eventuali errori di rete in catch().

Questo esempio è molto all'osso. In realtà dovremmo controllare il codice di risposta del server in response (se è un 404, o qualcosa d'altro), magari verificare che il mime type della risposta è quello che ci aspettiamo, se per esempio vogliamo trattare la risposta come JSON.

Le funzioni anonime

Piccola digressione: dato l'elevato numero di volte che ci troviamo a scrivere funzioni anonime come callback, le ultime versioni di javascript supportano una sintassi alternativa:

// vecchia sintassi
function(valore) {
    console.log(valore);
}

// nuova sintassi
valore => { 
    console.log(valore);
}

// più di un parametro
(val1, val2) => {
    console.log(val1, val2);
}

Async / Await

Le promesse sono promesse, ma non è che risolvano del tutto il nostro problema: c'è ancora una quantità di callback nel codice che fa paura.

Entriamo quindi nel regno delle ultime versioni di Javascript, supportato dalle ultime versioni dei browser, quindi fate i vostri conti se dovete sopp.. supportare Internet Explorer o comunque vecchie versioni di browser.

Le parole chiave sono async e await.

async definisce una funzione asincrona, che ritornerà automaticamente una promessa.

async function faQualcosaDiAsincrono() {
    ...
    return risultato;
}

faQualcosaDiAsincrono().then( r => { console.log(r); } );

await invece attende che una promessa ritornata da una funzione asincrona venga risolta. Puo' essere usato solo in funzioni definite con async.

await permette di scrivere codice asincrono che si legge molto facilmente.

Prendiamo l'ultima versione del nostro codice e scriviamola usando async/await:

btn.addEventListener('click', function(event){
    caricaDatiRemoti();
});

async function caricaDatiRemoti(){
    var code = await dialogoPromise("Codice");

    var response = await fetch("https://httpbin.org/get?code="+code);

    var data = await response.text();

    res.innerText = data;
}

Abbiamo spostato il nostro codice fuori dall gestore di eventi, in una nuova funzione asincrona che abbiamo chiamato caricaDatiRemoti(). Essendo una funzione asincrona, possiamo usare await per attendere la risoluzione delle promesse. Il codice è ora decisamente più pulito e leggibile.

Abbiamo pero' perso la gestione degli errori, ma con un semplice try..catch possiamo reinserirla:

async function caricaDatiRemoti(){
    try {
        var code = await dialogoPromise("Codice");

        var response = await fetch("https://httpbin.org/get?code="+code);

        var data = await response.text();

        res.innerText = data;

    } catch(err) {
        res.innerText = "Errore: " + err;
    }
}

(live)

Se, ad esempio, inseriamo un dominio inesistente in fetch() vedremo comparire il messaggio di errore.

Un'ultima nota: nel callback del gestore del click sul bottone, chiamiamo caricaDatiRemoti() e ne ignoriamo il risultato, non chiamiamo then() sulla promessa ritornata. Questo perchè non ci interessa: caricaDatiRemoti() non ritorna nessun valore ed esegue il suo compito completamente.

E questo è quanto

Abbiamo visto come riportare il nostro codice asincrono a una dimensione un po' più gestibile, dalla prima versione sincrona:

btn.addEventListener('click', function(event){
    var code = prompt("Codice:");
    var data = doXHRSync("https://httpbin.org/get?code="+code);

    res.innerText = data;
}

all'ultima, completamente asincrona:

async function caricaDatiRemoti(){
    try {
        var code = await dialogoPromise("Codice");

        var response = await fetch("https://httpbin.org/get?code="+code);

        var data = await response.text();

        res.innerText = data;

    } catch(err) {
        res.innerText = "Errore: " + err;
    }
}

le differenze sono davvero poche e la leggibilità del codice è praticamente identica.

Esistono altre tecniche, sviluppate prima della standardizzazione nel linguaggio di Promise e di async/await, per gestire codice asincrono. Non sempre sono compatibili tra di loro. Una di queste, per esempio, si basa sui generatori e su del codice extra, ma magari ne parliamo in un'altro articolo.