Comprendere le promesse JavaScript

Introduzione

Le promesse JavaScript possono essere difficili da comprendere. Pertanto, vorrei scrivere il modo in cui capisco le promesse.

Capire le Promesse

Una Promessa in breve:

“Immagina di essere un bambino. Tua mamma promette che ti regalerà un nuovo telefono la prossima settimana.”

Non sai se otterrai quel telefono fino alla prossima settimana. Tua mamma potrebbe davvero comprare un telefono nuovo di zecca, oppure non lo fa.

Questa è una promessa. Una promessa ha tre stati. Sono:

  1. In sospeso: Non sai se otterrai quel telefono
  2. Soddisfatta: Mamma è felice, ti compra un telefono nuovo di zecca
  3. Rifiutata: Mamma è infelice, non ti compra un telefono

Creare una Promessa

Convertiamo questo in JavaScript.

// ES5: Parte 1

var isMomHappy = false;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // soddisfatta
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // rifiutata
        }

    }
);

Il codice è abbastanza espressivo di per sé.

Ecco come normalmente appare la sintassi di una promise:

// la sintassi della promise è simile a questa
new Promise(function (resolve, reject) { ... } );

Utilizzare le Promises

Ora che abbiamo la promise, consumiamola:

// ES5: Parte 2

var willIGetNewPhone = ... // continua dalla parte 1

// chiamiamo la nostra promise
var askMom = function () {
    willIGetNewPhone
        .then(function (fulfilled) {
            // yay, hai ottenuto un nuovo telefono
            console.log(fulfilled);
             // output: { brand: 'Samsung', color: 'black' }
        })
        .catch(function (error) {
            // oops, la mamma non l'ha comprato
            console.log(error.message);
             // output: 'la mamma non è felice'
        });
};

askMom();

Eseguiamo l’esempio e vediamo il risultato!

Demo: https://jsbin.com/nifocu/1/edit?js,console

Concatenazione di Promesse

Le promesse sono concatenabili.

Diciamo che tu, il bambino, prometti al tuo amico che gli mostrerai il nuovo telefono quando tua mamma te ne compra uno.

Questa è un’altra promessa. Scriviamola!

// ES5

// 2a promessa
var showOff = function (phone) {
    return new Promise(
        function (resolve, reject) {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

Note: Possiamo accorciare il codice precedente scrivendo come di seguito:

// accorciamolo

// 2a promessa
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

Concateniamo le promesse. Tu, il bambino, puoi iniziare la promessa showOff solo dopo la promessa willIGetNewPhone.

// chiama la nostra promessa
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // concateniamola qui
    .then(function (fulfilled) {
            console.log(fulfilled);
         // output: 'Ehi amico, ho un nuovo telefono Samsung nero.'
        })
        .catch(function (error) {
            // ops, mamma non lo compra
            console.log(error.message);
         // output: 'mamma non è felice'
        });
};

È così che puoi concatenare la promessa.

Le promesse sono asincrone

Le promesse sono asincrone. Registriamo un messaggio prima e dopo aver chiamato la promessa.

// chiamiamo la nostra promessa
var askMom = function () {
    console.log('before asking Mom'); // log prima
    willIGetNewPhone
        .then(showOff)
        .then(function (fulfilled) {
            console.log(fulfilled);
        })
        .catch(function (error) {
            console.log(error.message);
        });
    console.log('after asking mom'); // log dopo
}

Qual è la sequenza di output attesa? Potresti aspettarti:

1. before asking Mom
2. Hey friend, I have a new black Samsung phone.
3. after asking mom

Tuttavia, la sequenza di output effettiva è:

1. before asking Mom
2. after asking mom
3. Hey friend, I have a new black Samsung phone.

Non ti fermeresti mentre aspetti la promessa di tua mamma (il nuovo telefono). Questo è qualcosa che chiamiamo asincrono: il codice verrà eseguito senza bloccare o attendere il risultato. Tutto ciò che deve aspettare che una promessa proceda viene messo in .then.

Ecco l’esempio completo in ES5:

// ES5: Esempio completo

var isMomHappy = true;

// Promise
var willIGetNewPhone = new Promise(
    function (resolve, reject) {
        if (isMomHappy) {
            var phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone); // soddisfatta
        } else {
            var reason = new Error('mom is not happy');
            reject(reason); // rifiutata
        }

    }
);

// 2a promise
var showOff = function (phone) {
    var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

    return Promise.resolve(message);
};

// chiamiamo la nostra promise
var askMom = function () {
    willIGetNewPhone
    .then(showOff) // concateniamola qui
    .then(function (fulfilled) {
            console.log(fulfilled);
            // output: 'Hey amico, ho un nuovo telefono Samsung nero.'
        })
        .catch(function (error) {
            // ops, mamma non l'ha comprato
            console.log(error.message);
            // output: 'mamma non è felice'
        });
};

askMom();

Promises in ES5, ES6/2015, ES7/Next

ES5 – Maggioranza dei browser

Il codice demo è funzionante in ambienti ES5 (tutti i principali browser + NodeJs) se includi la libreria promise Bluebird. Questo perché ES5 non supporta le promises di default. Un’altra famosa libreria promise è Q di Kris Kowal.

ES6 / ES2015 – Browser moderni, NodeJs v6

Il codice demo funziona subito perché ES6 supporta nativamente le promesse. Inoltre, con le funzioni ES6, possiamo ulteriormente semplificare il codice con una funzione freccia e utilizzare const e let.

Ecco l’esempio completo in codice ES6:

//_ ES6: Esempio completo_

const isMomHappy = true;

// Promessa
const willIGetNewPhone = new Promise(
    (resolve, reject) => { // funzione freccia
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 2a promessa
const showOff = function (phone) {
    const message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';
    return Promise.resolve(message);
};

// chiamiamo la nostra promessa
const askMom = function () {
    willIGetNewPhone
        .then(showOff)
        .then(fulfilled => console.log(fulfilled)) // funzione freccia
        .catch(error => console.log(error.message)); // funzione freccia
};

askMom();

Nota che tutti i var sono sostituiti con const. Tutti i function(resolve, reject) sono stati semplificati in (resolve, reject) =>. Queste modifiche portano alcuni benefici.

ES7 – Async/Await

ES7 ha introdotto la sintassi async e await. Rende la sintassi asincrona più facile da comprendere, senza i .then e .catch.

Riscriviamo il nostro esempio con la sintassi ES7:

// ES7: Esempio completo
const isMomHappy = true;

// Promessa
const willIGetNewPhone = new Promise(
    (resolve, reject) => {
        if (isMomHappy) {
            const phone = {
                brand: 'Samsung',
                color: 'black'
            };
            resolve(phone);
        } else {
            const reason = new Error('mom is not happy');
            reject(reason);
        }

    }
);

// 2a promessa
async function showOff(phone) {
    return new Promise(
        (resolve, reject) => {
            var message = 'Hey friend, I have a new ' +
                phone.color + ' ' + phone.brand + ' phone';

            resolve(message);
        }
    );
};

// chiamiamo la nostra promessa in stile ES7 async await
async function askMom() {
    try {
        console.log('before asking Mom');

        let phone = await willIGetNewPhone;
        let message = await showOff(phone);

        console.log(message);
        console.log('after asking mom');
    }
    catch (error) {
        console.log(error.message);
    }
}

// async await anche qui
(async () => {
    await askMom();
})();

Promesse e Quando Utilizzarle

Perché abbiamo bisogno delle promesse? Come appariva il mondo prima delle promesse? Prima di rispondere a queste domande, torniamo ai fondamentali.

Funzione Normale VS Funzione Asincrona

Diamo un’occhiata a questi due esempi. Entrambi gli esempi eseguono l’addizione di due numeri: uno utilizza funzioni normali, l’altro aggiunge in remoto.

Funzione Normale per Aggiungere Due Numeri

// aggiungere due numeri normalmente

function add (num1, num2) {
    return num1 + num2;
}

const result = add(1, 2); // ottieni result = 3 immediatamente
Funzione Asincrona per Aggiungere Due Numeri
// aggiungere due numeri in remoto

// ottieni il risultato chiamando un'API
const result = getAddResultFromServer('http://www.example.com?num1=1&num2=2');
// ottieni result = "undefined"

Se aggiungi i numeri con la funzione normale, ottieni il risultato immediatamente. Tuttavia, quando effettui una chiamata remota per ottenere il risultato, devi aspettare e non puoi ottenere il risultato immediatamente.

Non sai se otterrai il risultato perché il server potrebbe essere down, lento nella risposta, ecc. Non vuoi che l’intero processo sia bloccato mentre aspetti il risultato.

Chiamare API, scaricare file e leggere file sono alcune delle operazioni asincrone usuali che eseguirai.

Non è necessario utilizzare le promesse per una chiamata asincrona. Prima delle promesse, utilizzavamo i callback. I callback sono una funzione che chiami quando ottieni il risultato di ritorno. Modifichiamo l’esempio precedente per accettare un callback.

// somma due numeri in remoto
// ottieni il risultato chiamando un'API

function addAsync (num1, num2, callback) {
    // utilizza l'API callback getJSON di jQuery
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback
    const result = success; // ottieni result = 3 qui
});

Azione Asincrona Successiva

Invece di aggiungere i numeri uno alla volta, vogliamo aggiungere tre volte. In una funzione normale, lo faremmo:-

// somma due numeri normalmente

let resultA, resultB, resultC;

 function add (num1, num2) {
    return num1 + num2;
}

resultA = add(1, 2); // ottieni resultA = 3 immediatamente
resultB = add(resultA, 3); // ottieni resultB = 6 immediatamente
resultC = add(resultB, 4); // ottieni resultC = 10 immediatamente

console.log('total' + resultC);
console.log(resultA, resultB, resultC);

Ecco come appare con i callback:

// somma due numeri in remoto
// ottieni il risultato chiamando un'API

let resultA, resultB, resultC;

function addAsync (num1, num2, callback) {
    // utilizza l'API callback getJSON di jQuery
	// https://api.jquery.com/jQuery.getJSON/
    return $.getJSON('http://www.example.com', {
        num1: num1,
        num2: num2
    }, callback);
}

addAsync(1, 2, success => {
    // callback 1
    resultA = success; // qui ottieni result = 3

    addAsync(resultA, 3, success => {
        // callback 2
        resultB = success; // qui ottieni result = 6

        addAsync(resultB, 4, success => {
            // callback 3
            resultC = success; // qui ottieni result = 10

            console.log('total' + resultC);
            console.log(resultA, resultB, resultC);
        });
    });
});

Demo: https://jsbin.com/barimo/edit?html,js,console

Questa sintassi è meno user-friendly a causa dei callback annidati in profondità.

Evitare Callbacks Annidati in Profondità

Le Promises possono aiutarti a evitare i callback annidati in profondità. Diamo un’occhiata alla versione con Promises dello stesso esempio:

// somma due numeri in remoto utilizzando observable

let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // utilizza l'API fetch di ES6, che restituisce una promessa
	// Cos'è .json()? https://developer.mozilla.org/en-US/docs/Web/API/Body/json
    return fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json()); 
}

addAsync(1, 2)
    .then(success => {
        resultA = success;
        return resultA;
    })
    .then(success => addAsync(success, 3))
    .then(success => {
        resultB = success;
        return resultB;
    })
    .then(success => addAsync(success, 4))
    .then(success => {
        resultC = success;
        return resultC;
    })
    .then(success => {
        console.log('total: ' + success)
        console.log(resultA, resultB, resultC)
    });

Con le promesse, appiattiamo il callback con .then. In un certo senso, sembra più pulito perché non c’è nidificazione di callback. Con la sintassi async di ES7, potresti migliorare ulteriormente questo esempio.

Observables

Prima di decidere per le promesse, c’è qualcosa che è emerso per aiutarti a gestire i dati asincroni chiamato Observables.

Diamo un’occhiata allo stesso demo scritto con Observables. In questo esempio, utilizzeremo RxJS per gli observables.

let Observable = Rx.Observable;
let resultA, resultB, resultC;

function addAsync(num1, num2) {
    // utilizza l'API fetch di ES6, che restituisce una promessa
    const promise = fetch(`http://www.example.com?num1=${num1}&num2=${num2}`)
        .then(x => x.json());

    return Observable.fromPromise(promise);
}

addAsync(1,2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total: ' + x)
    console.log(resultA, resultB, resultC)
  });

Gli Observables possono fare cose più interessanti. Ad esempio, delay aggiungere la funzione di 3 secondi con una sola riga di codice o riprovare in modo da poter riprovare una chiamata un certo numero di volte.

...

addAsync(1,2)
  .delay(3000) // ritardo di 3 secondi
  .do(x => resultA = x)
  ...

Potresti leggere uno dei miei post su RxJs qui.

Conclusione

Familiarizzare con callback e promesse è importante. Comprendili e utilizzali. Non preoccuparti ancora degli Observable. Tutti e tre possono influenzare il tuo sviluppo a seconda della situazione.

Source:
https://www.digitalocean.com/community/tutorials/understanding-javascript-promises