Promises Javascript, gestire processi asincroni in modo sincrono

Le promise ci permettono di gestire processi asincroni in modo più sincrono, possiamo anche gestire il loro valore in futuro.

Contents

Creazione di una Promise

Per prima cosa instanziamo la Promise e prevediamo due metodiuno per risolvere la promise, uno per rifiutarla. Per ora prevediamo solo il metodo resolve, e impostiamo, sempre temporaneamente, un setTimeout che simuli una chiamata http o qualsiasi altra operazione che richieda tempo per essere eseguita:

<script>
var p = new Promise(function(resolve, reject){
    setTimeout(() => {
        resolve("ok ce l'abbiamo fatta, il numero era " + num);
    }, 5000);
});
p.then(res => {
    document.getElementsByTagName('body')[0].innerHTML = res;
});
</script>

A questo punto una volta passati i 5 secondi impostati dal setTimeout andremo a chiamare il metodo resolve, passandogli come parametro la risposta della nostra chiamata o della nostra elaborazione, quindi nel metodo then, andremo a stampare la nostra risposta nel body. Se tutto funzionerà aperta la nostra pagina sarà completamente bianca ma dopo 5 secondi verrà inserita all’interno del body la stringa che abbiamo passato.

Ora andiamo ad impostare anche reject. prendiamo un numero casuale da 0 a 10 e se il numero è maggiore di quattro la promise sarà risolta altrimenti sarà rigettata :

<script>
var p = new Promise(function(resolve, reject){
    setTimeout(() => {
        var num = Math.round(10*Math.random());
        if (num > 4){
            resolve("ok ce l'abbiamo fatta, il numero era " + num);
        } else {
            reject('chiamata fallita il numero era '+ num )
        }

    }, 5000);
});
p.then(res => {
    document.getElementsByTagName('body')[0].innerHTML = res;
}).catch((err) => {
    document.getElementsByTagName('body')[0].innerHTML = err;
})

</script>

Tramite il catch andremo a gestire la promise rifiutata. Chiaramente nel caso di una chiamata http possiamo tranquillamente risolvere la promise se la chiamata è andata a buon fine altrimenti rifiutarla.

Gli stati di una promise sono tre, la promise può essere:

  • In pending, pendente, è quando la promise non è stata nè risolta nè rigettata, nel nostro esempio precendente era la fase in cui non erano ancora passati i 5 secondi  del nostro setTimeout
  • Fullfilled, risolta, è quando la nostra promise è stata risolta, cioè è stato chiamato il metodo resolve
  • Rejected, rifiutata, quando questa fallisce, cioè è stato chiamato il metodo reject

Chiamata ajax – javascript

Realizziamo ora una semplice chiamata ajax con javascript, che successivamente cambieremo in una Promise.
Andiamo a creare un file php che ci ritorni dei dati.  Mettiamoci dentro un codice simile a questo e chiamiamo la pagina index.php:

<?php
$data = [ [ 'name' => 'Massimiliano',
    'surname' => 'Salerno'
],
    [
        'name' => 'Mario',
        'surname' => 'Rossi'
    ],
    [
        'name' => 'Luca',
        'surname' => 'Bianchi'
    ]
];
header('Content-type:application/json');

echo json_encode($data);

exit;

Adesso dal terminale andiamo sulla cartella del nostro progetto e facciamo partire un server momentaneo in questo modo:

php -S localhost:8080 

Questo comando mette su un server temporaneo che possiamo utilizzare, se sul nostro browser digitiamo ‘localhost:8080’ vedremo il json prodotto dalla pagina php. Ora andiamo a creare una pagina chiamata ajax.html e un file ajax.js. Nel body della nostra pagina inseriamo il seguente codice:

<div id="content"></div>
<script src="ajax.js"></script>

Mentre nel file ajax.js andiamo a scrivere il javascript per la chiamata ajax. Per prima cosa instanziamo l’oggetto XMLHttpRequest:

window.onload = function () {
    let xhr = new XMLHttpRequest();
}

Ora con il metodo open andiamo a stabilire il metodo della chiamata, come primo parametro e come secondo l’url di destinazione, subito dopo chiamiamo il metodo send per inviare i dati, senza passargli parametri nel nostro caso perchè non ne abbiamo bisogno:

xhr.open('GET', 'http://localhost:8080/index.php');
xhr.send();

È arrivato il momento di metterci in ascolto del momento in cui la chiamata ajax sarà pronta. Usiamo per questo onreadystatechange, e andiamo a verificare che il readyState sia uguale a 4 (che vuol dire che la chiamata è già finita) e che lo stato sia 200, poi andiamo a prelevare la response e stamparla in console:

<div id="content"></div>
xhr.onreadystatechange = function () {
    if(xhr.readyState === 4 && xhr.status === 200){
        let res = JSON.parse(xhr.responseText);
       console.log(res);
    }
};

Chiamata ajax tramite Promises

Ed ora trasformiamo la stessa identica chiamata con la promise in questo modo:

let p = new Promise(function (resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:8080/index.php');
    xhr.send();
    xhr.onreadystatechange = function () {
        if(xhr.readyState === 4 && xhr.status === 200){
            let res = JSON.parse(xhr.responseText);
            resolve(res);
        }
    };
});

p.then(function (res) {
    console.log(res);
})

Andiamo a creare una promise e la risolviamo nel momento in cui la chiamata è terminata, chiamando il metodo resolve. Al metodo resolve passiamo i dati recuperati dalla chiamata, che gestiremo poi nel then, dove per il momento andremo solamente a stampare in console la risposta della chiamata. Possiamo provarlo sul nostro browser all’indirizzo http://localhost:8080/ajax.html e avremo lo stesso risultato di prima, solo che con la Promise. Ora pensiamo a gestire l’errore:

let p = new Promise(function (resolve, reject) {
    let xhr = new XMLHttpRequest();
    xhr.open('GET', 'http://localhost:8080/index.php');
    xhr.send();
    xhr.onreadystatechange = () => {
        if(xhr.readyState === 4){
            if(xhr.status === 200){
                let res = JSON.parse(xhr.responseText);
                resolve(res);
            } else {
                reject('Lo status è diverso da 200')
            }

        }
    };

});

p.then(res => {
    console.log(res);
}).catch(err => {
    console.log(err);
})

Se lo stato è diverso da 200, andiamo a chiamare il metodo reject che gestiamo con il catch, e visto che siamo in piena versione di Javascript ES6 ne abbiamo approfittato per mettere al posto delle funzioni anonime le nuove arrow function.

Fetch: chiamate ajax e promise

Continuamo il nostro progetto, andremo a semplificare di molto la nostra chiamata grazie all’utilizzo di fetch. Questo metodo ritorna una promise, che verrà risolta, anche in caso di errori di status 404, 500 ecc… l’unico caso in cui viene rigettata è un errore di connessione.

Se vogliamo che tutto funzioni anche su browser un po’ meno moderni ci converrà caricare anche il polyfill.

Possiamo andare a commentare per ora tutta la parte di codice javascript che abbiamo scritto finora e inserire nel file ajax.js il seguente codice:

fetch('http://localhost:8080/index.php')
.then(res => console.log(res))
.catch(err=> console.log('Errore di connessione'));

Iniziamo con l’utilizzare la forma più semplice di fetch, che consiste nel passargli solamente l’url della chiamata. Di default se non gli passiamo altri parametri fetch effettuerà la chiamata in GET.
Il metodo fetch ci ritornerà una promise e quindi la gestiamo con then, mentre il possibile errore di connessione lo gestiamo con catch. Nel then avremo a questo punto la response, positiva o negativa che sia e a questo punto dovremmo gestirla.
La proprietà ok è un boolean che si trova nella response e ci dice l’esito positivo o meno della chiamata.

Come vediamo dall’immagine sopra, possiamo avere accesso all’oggetto headers, allo status della risposta, tramite la proprietà status e allo stato ok, che restituirà un true o false a seconda dell’esito della risposta, sarà true solamente se la risposta sarà tra 200 e 299 altrimenti ritornerà false.
A questo punto possiamo gestire la risposta andando a controllare che il suo esito sia positivo:

fetch('http://localhost:8080/index.php').then(res => {
    if(res.ok){
        console.log(res.headers.get('Content-Type'));
    }
    console.log(res)
}).catch(err => {
    console.log('Errore di connessione');
});

Con l’if siamo andati a controllare che la chiamata sia andata a buon fine, e andiamo a stampare l’header che avevamo impostato prima nella pagina index.php.

Una volta verificato sul nostro browser che tutto funzioni, possiamo passare allo step successivo, a questo punto verifichiamo se l’header è quello giusto e trasformiamo in json la response:

if(res.ok){
    if(res.headers.get('Content-Type').includes('application/json')){
        return res.json();
    }
}

Abbiamo diversi metodi di leggere i contenuti, questi metodi restituiscono una promise, abbiamo utlizzato .json trattandosi appunto di formato json.
Ritornando una promise abbiamo ancora una promise da risolvere, quindi abbiamo bisogno di un ulteriore then. Il codice diverrà questo:

fetch('http://localhost:8080/index.php').then(res => {
    if(res.ok){
        if(res.headers.get('Content-Type').includes('application/json')){
            return res.json();
        }
    }
    console.log(res)
}).then(json =>{
    console.log(json);
}).catch(err => {
    console.log('Errore di connessione');
});

Ora se dobbiamo settare gli headers oppure decidere il metodo della chiamata, possiamo scrivere in questo modo:

let headers = new Headers();
headers.append('Authorization', 'Bearer fdskjfbsdfb');
let init = {
    headers: headers,
    method: 'GET',
};
fetch('http://localhost:8080/index.php', init).then(res => {
    if(res.ok){
        if(res.headers.get('Content-Type').includes('application/json')){
            console.log(res);
            return res.json();
        }
    }
}).then(json => {
    console.log(json);
}).catch(err => {
    console.log('Errore di connessione');
});

Possiamo anche crearci prima una Request e poi passarla al fetch in questo modo:

let headers = new Headers();
headers.append('Authorization', 'Bearer fdskjfbsdfb');
let init = {
    headers: headers,
    method: 'GET',
};
var myRequest = new Request('http://localhost:8080/index.php', init);
fetch(myRequest).then(res => {
    if(res.ok){
        if(res.headers.get('Content-Type').includes('application/json')){
            return res.json();
        }
    }
}).then(json =>{
    console.log(json);
}).catch(err => {
    console.log('Errore di connessione');
});

Promise.all(): eseguire diverse promise in parallelo

Promise.all() ci consente di eseguire diverse promise in parallelo e avere tutti i dati alla conclusione dell’ultima promise, non importa quale sarà risolta per prima, noi le processeremo tutte insieme una volta terminate tutte.
Questo si fa passando come parametro un array di promise, se una promise dell’array viene rigettata blocca tutte le altre promise in esecuzione.
Andiamo a testarlo provando con il setTimeout perchè tutto sia più chiaro, passiamo i risultati del nostro fetch ad un’altra funzione che andrà a creare due promise abbastanza lunghe e complicate, che richiederanno una 5 secondi, l’altra 10.
Noi dovremmo svolgere delle operazioni quando entrambe queste promise saranno terminate, ma nello stesso tempo non vogliamo far partire la seconda promise dopo che ha finito la prima, ma contemporaneamente, in modo che tutto sia più veloce. Tanto nessuna delle due promise dipende dall’altra. Iniziamo quindi a richiamare una funzione al termine del nostro fetch risolto:

let headers = new Headers();
headers.append('Authorization', 'Bearer fdskjfbsdfb');
let init = {
    headers: headers,
    method: 'GET',
};
fetch('http://localhost:8080/index.php', init).then(res => {
    if(res.ok){
        if(res.headers.get('Content-Type').includes('application/json')){
            console.log(res);
            return res.json();
        }
    }
}).then(json => {
    promiseAll(json);
}).catch(err => {
    console.log('Errore di connessione');
});

Possiamo anche crearci prima una Request e poi passarla al fetch in questo modo:

fetch(myRequest).then(res => {
    if(res.ok){
        if(res.headers.get('Content-Type').includes('application/json')){
            console.log(res);
            return res.json();
        }
    }
}).then(json =>{
    promiseAll(json);
})

Quindi praticamente ora nel then il risultato della promise creata con fetch lo passiamo alla funzione che ora andremo a creare che si chiama promiseAll, ecco il contenuto della funzione:

function promiseAll(list) {
    var p = new Promise((resolve, reject) => {
        setTimeout(()=> {
            console.log('p è stato risolto');
            resolve(list.map(ele => ele.name));
        }, 5000)
    });

    var p2 = new Promise((resolve, reject) => {
        setTimeout(()=> {
            console.log('p2 è stato risolto');
            resolve(list.map(ele => ele.surname));
        }, 10000)
    });

    Promise.all([p, p2]).then(res => {
        document.getElementsByTagName('body')[0].innerHTML = JSON.stringify(res);
    })
}

Ricordiamoci che questa chiamata alla nostra pagina index.php ci torna un elenco di persone con nome e cognome quindi facciamo finta che dopo una serie di operazioni, magari di verifiche con chiamate al server ci ritorni un json con ulteriori informazioni, per ora setteremo semplicemente due setTimeout dimostrativi.
In questo caso tramite il metodo map andiamo a far tornare un array con i soli nomi, nella seconda promise un array con i soli cognomi. La prima promise ha un timeout di 5 secondi, quindi verrà risolta dopo 5 secondi, la seconda invece ne impiegherà 10, mettiamo un console.log in modo che possiamo vedere bene quando termina la prima promise e constatare che per dare il risultato aspetta la seconda.

Se andiamo ad eseguire il tutto vedremo che dopo 5 secondi verrà risolta la prima e avremo in console il messaggio ‘p è stato risolto‘, a questo punto si attenderà la risoluzione della seconda promise, che nel frattempo è già in esecuzione, son già passati i primi 5 secondi, ne dovremmo aspettare solo altri 5terminata la seconda promise finalmente sarà eseguito il then dopo la promise.all.

A questo punto res conterrà un array con le due response, in questo caso avremo un array che conterrà un array con i nomi e un array con i cognomi, in questo modo:

[["Massimiliano","Mario","Luca"],["Salerno","Rossi","Bianchi"]]

Ora andiamo a mettere un controllo per evitare che list sia vuoto e in quel caso rigettare la promise e tramite il catch tornare un messaggio:

function promiseAll(list) {
    var p = new Promise((resolve, reject) => {
        setTimeout(()=> {
            if(list && list.length){
                resolve(list.map(ele => ele.name));
            } else {
                reject({message: "La lista di nomi è vuota"});
            }
        }, 5000)
    });

    var p2 = new Promise((resolve, reject) => {
        setTimeout(()=> {
            if(list && list.length){
                resolve(list.map(ele => ele.surname));
            } else {
                reject({message: "La lista di cognomi è vuota"});
            }

        }, 10000)
    });

    Promise.all([p, p2]).then(res => {
        document.getElementsByTagName('body')[0].innerHTML = JSON.stringify(res);
    }).catch(err => {
        document.getElementsByTagName('body')[0].innerHTML = JSON.stringify(err);
    }).finally(() => {
        alert('Terminata la promise');
    });
}

Se tutto è andato bene nulla è cambiato. Ora facciamo in modo che la prima promise fallisca, vedremo che in realtà la seconda promise sarà bloccata e si attiverà direttamente il catch di promise.all.
Abbiamo anche aggiunto il finally, questo sarà richiamata sia quando la promise viene risolta che quando viene rigettata, in questo caso ci mostrerà un semplice alert.
Per far fallire la prima promise possiamo risettare list come array vuoto in questo modo:

setTimeout(()=> {
    list = [];
    if(list && list.length){
        resolve(list.map(ele => ele.name));
            } else {
        reject({message: "La lista di nomi è vuota"});
            }
}, 5000);

Ecco che la prima promise non chiamerà più resolve ma reject e quindi si andrà direttamente al catch, e qui verrà inviato il messaggio ‘la lista di nomi è vuota‘, chiaramente nel caso in cui spostiamo la definizione di list ad array vuoto nella seconda promise, il risultato che avremo al catch sarà un messaggio che riporta ‘la lista di cognomi è vuota‘.

Ora per vedere la semplicità della cosa e l’uso con fetch andiamo anche a fare due chiamate http e ci aiuteremo con jsonplaceholder, che ci consentirà di avere automaticamente dei fake json.
Per prima cosa, per vedere in pagina sia il risultato di quel che abbiamo fatto finora sia di quello che ora andremo a fare, inseriamo due div all’interno dell’html della nostra pagina ajax.html:

<div id="promise"></div>
<div id="content"></div>

Ora andiamo a modificare la stampa precedente, avevamo stampato direttamente nel body il contenuto, facciamolo invece nel primo div con id promise:

Promise.all([p, p2]).then(res => {
    document.getElementById('promise').innerHTML = JSON.stringify(res);
}).catch(err => {
    document.getElementById('promise').innerHTML = JSON.stringify(err);
});

Ora procediamo con la nuova promise, guardate che semplicità:

var url = "https://jsonplaceholder.typicode.com/albums";

let album = fetch(url+'/2').then(res =>  res.json());
let photos = fetch(url+'/2/photos').then(res =>  res.json());
Promise.all([album, photos]).then(res => {
    console.log(res);
}).catch(err  => {
    console.log(err);
});

Il nostro risultato l’abbiamo, abbiamo l’album e l’elenco dell’immagini collegate, ora andiamo a metterlo in pagina con un minimo di grafica:

Promise.all([album, photos]).then(res => {
    let album = res[0];
    let photos = res[1];
    var html = `
        <header>
            <h1>${album.title}</h1>
        </header>
        <main class="container">
    `;
    photos.forEach(photo => {
        html += `
            <figure>
                <img src="${photo.thumbnailUrl}" alt="${photo.title}" title="${photo.title}">
                <figcaption>${photo.title}</figcaption>
            </figure>
        `;
    });
    html += "</main>"
    document.getElementById('content').innerHTML = html;
})

Quindi abbiamo preso il primo elemento dell’array che è la risposta della prima promise, cioè la chiamata all’album, e il secondo elemento dell’array che sono l’insieme delle foto associate all’album. Chiaramente l’ordine è dettato dall’ordine dell’array passato a promise.all.
Quindi siamo andati a stampare l’header e a metterci il titolo dell’album, prelevato quindi dalla prima promise, poi abbiamo caricato l’insieme delle immagini collegate all’album. Se verifichiamo sul browser tutto dovrebbe essere come ci aspettiamo.
Visto che ci stiamo diamo anche un minimo di stile alla pagina, con un po’ di css, anche se non è ora il nostro obiettivo. Ma niente di particolare, niente che richieda troppo tempo, aggiungiamo direttamente nell’head della pagina lo style:

<style>
header h1 {
    text-align: center;
  text-transform: uppercase;
}
main figure{
    text-align: center;
  width: 31%;
  float: left;
  margin: 1%;
  border-radius: 20px;
  background-color: #e4e4e4;
  padding: 10px;
  box-sizing: border-box;
  height: 300px;
}
main figure figcaption{
    padding: 10px;
  margin-top: 10px;
}
</style>

async e await

Una funzione async è una funzione asincrona che torna una promise, all’interno di questo tipo di funzione possiamo utilizzare il costrutto await che mette in pausa l’esecuzione della funzione, fino a che la promise davanti alla quale mettiamo await non è risolta o rigettata.
Andiamo a modificare la nostra prima promise fatta, quella con il setTimeout. Per funzionare ha bisogno che gli passiamo la lista di persone prelevata con la chiamata Http, modifichiamo il codice con le nuove nozioni imparate:

firstCall().then(res=> {
    document.getElementById('beforePromise').innerHTML = JSON.stringify(res);
});

async function firstCall() {
    let headers = new Headers();
    headers.append('Authorization', 'Bearer fdskjfbsdfb');
    let init = {
        headers: headers,
        method: 'GET',
    };
    var myRequest = new Request('http://localhost:8080/index.php', init);
    var json = await fetch(myRequest).then(res => {
        if(res.ok){
            if(res.headers.get('Content-Type').includes('application/json')){
                return res.json();
            }
        }
    });

    promiseAll(json);

    return json;

}

Abbiamo messo il nostro vecchio codice in una funzione async gli abbiamo aggiunto oltre alla chiamata della funzione promiseAll anche un return, che possiamo poi gestire per far ritornare alla funzione una risposta che potremo poi gestire, andandola ad esempio a stamparla nel DOM dove desideriamo.
Nella variabile json andiamo a mettere il risultato del fetch, aspettando che la promise generata dal metodo fetch sia risolta. Senza await, la variabile json risulterebbe undefined sia nel return che nel parametro passato alla funzione promiseAll.

In questo modo siamo riusciti a gestire in maniera sincrona un processo asincrono. Tutto questo in modo molto semplice.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *