Promesse JavaScript: un tutorial con esempi

Pubblicato: 2022-03-11

Le promesse sono un argomento caldo nei circoli di sviluppo JavaScript e dovresti assolutamente conoscerle. Non sono facili da avvolgere la testa; possono essere necessari alcuni tutorial, esempi e una discreta quantità di pratica per comprenderli.

Il mio obiettivo con questo tutorial è aiutarti a capire le promesse JavaScript e spingerti a esercitarti di più. Spiegherò cosa sono le promesse, quali problemi risolvono e come funzionano. Ogni passaggio, descritto in questo articolo, è accompagnato da un esempio di codice jsbin per aiutarti a lavorare e da usare come base per ulteriori esplorazioni.

Le promesse JavaScript sono spiegate in questo tutorial completo.

Che cos'è una promessa JavaScript?

Una promessa è un metodo che alla fine produce un valore. Può essere considerato come la controparte asincrona di una funzione getter. La sua essenza può essere spiegata come:

 promise.then(function(value) { // Do something with the 'value' });

Le promesse possono sostituire l'uso asincrono dei callback e offrono diversi vantaggi su di essi. Iniziano a guadagnare terreno man mano che sempre più librerie e framework li abbracciano come il modo principale per gestire l'asincronia. Ember.js è un ottimo esempio di tale framework.

Esistono diverse librerie che implementano la specifica Promises/A+. Impareremo il vocabolario di base e lavoreremo attraverso alcuni esempi di promesse JavaScript per introdurre i concetti alla base in modo pratico. Userò una delle librerie di implementazione più popolari, rsvp.js, negli esempi di codice.

Preparati, lanceremo tanti dadi!

Ottenere la libreria rsvp.js

Promises, e quindi rsvp.js, possono essere utilizzati sia sul server che sul lato client. Per installarlo per nodejs , vai alla cartella del tuo progetto e digita:

 npm install --save rsvp

Se lavori sul front-end e usi bower, è solo un

 bower install -S rsvp

lontano.

Se vuoi solo entrare subito nel gioco, puoi includerlo tramite un semplice tag di script (e con jsbin , puoi aggiungerlo tramite il menu a discesa "Aggiungi libreria"):

 <script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>

Quali proprietà ha una promessa?

Una promessa può trovarsi in uno dei tre stati: in attesa , soddisfatta o rifiutata . Quando viene creata, la promessa è in stato di attesa. Da qui, può passare allo stato soddisfatto o rifiutato. Chiamiamo questa transizione la risoluzione della promessa . Lo stato risolto di una promessa è il suo stato finale, quindi una volta che viene soddisfatta o rifiutata, rimane lì.

Il modo per creare una promessa in rsvp.js è tramite quello che viene chiamato un costruttore rivelatore. Questo tipo di costruttore prende un singolo parametro di funzione e lo chiama immediatamente con due argomenti, fulfill e reject , che possono trasferire la promessa allo stato fulfilled o rejected :

 var promise = new RSVP.Promise(function(fulfill, reject) { (...) });

Questo modello di promesse JavaScript è chiamato costruttore rivelatore perché l'argomento della funzione singola rivela le sue capacità alla funzione di costruzione, ma garantisce che i consumatori della promessa non possano manipolarne lo stato.

I consumatori della promessa possono reagire ai suoi cambiamenti di stato aggiungendo il loro gestore attraverso il metodo then . Richiede una funzione di gestione dell'adempimento e una di rifiuto, entrambe possono mancare.

 promise.then(onFulfilled, onRejected);

A seconda del risultato del processo di risoluzione della promessa, il gestore onFulfilled o onRejected viene chiamato in modo asincrono .

Vediamo un esempio che mostra in quale ordine vengono eseguite le cose:

 function dieToss() { return Math.floor(Math.random() * 6) + 1; } console.log('1'); var promise = new RSVP.Promise(function(fulfill, reject) { var n = dieToss(); if (n === 6) { fulfill(n); } else { reject(n); } console.log('2'); }); promise.then(function(toss) { console.log('Yay, threw a ' + toss + '.'); }, function(toss) { console.log('Oh, noes, threw a ' + toss + '.'); }); console.log('3');

Questo frammento di codice stampa un output simile al seguente:

 1 2 3 Oh, noes, threw a 4.

Oppure, se siamo fortunati, vediamo:

 1 2 3 Yay, threw a 6.

Questo tutorial sulle promesse dimostra due cose.

Primo, che i gestori che abbiamo allegato alla promessa sono stati effettivamente chiamati dopo che tutto l'altro codice è stato eseguito, in modo asincrono.

In secondo luogo, che il gestore dell'adempimento è stato chiamato solo quando la promessa è stata mantenuta, con il valore con cui è stata risolta (nel nostro caso, il risultato del lancio dei dadi). Lo stesso vale per il gestore del rifiuto.

Incatenare promesse e gocciolare

La specifica richiede che anche la funzione then (i gestori) debba restituire una promessa, che consente di concatenare le promesse insieme, risultando in un codice che sembra quasi sincrono:

 signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)

Qui, signupPayingUser restituisce una promessa e ogni funzione nella catena della promessa viene chiamata con il valore restituito del gestore precedente una volta completata. Per tutti gli scopi pratici, questo serializza le chiamate senza bloccare il thread di esecuzione principale.

Per vedere come ogni promessa viene risolta con il valore di ritorno dell'elemento precedente nella catena, torniamo a lanciare i dadi. Vogliamo lanciare i dadi un massimo di tre volte, o fino a quando non escono i primi sei jsbin:

 function dieToss() { return Math.floor(Math.random() * 6) + 1; } function tossASix() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; if (n === 6) { fulfill(n); } else { reject(n); } }); } function logAndTossAgain(toss) { console.log("Tossed a " + toss + ", need to try again."); return tossASix(); } function logSuccess(toss) { console.log("Yay, managed to toss a " + toss + "."); } function logFailure(toss) { console.log("Tossed a " + toss + ". Too bad, couldn't roll a six"); } tossASix() .then(null, logAndTossAgain) //Roll first time .then(null, logAndTossAgain) //Roll second time .then(logSuccess, logFailure); //Roll third and last time

Quando esegui questo codice di esempio di promesse, vedrai qualcosa del genere sulla console:

 Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.

La promessa restituita da tossASix viene rifiutata quando il lancio non è un sei, quindi il gestore del rifiuto viene chiamato con il lancio effettivo. logAndTossAgain stampa il risultato sulla console e restituisce una promessa che rappresenta un altro lancio di dadi. Quel lancio, a sua volta, viene anche rifiutato e disconnesso dal prossimo logAndTossAgain .

A volte, però, sei fortunato*, e riesci a tirare un sei:

 Tossed a 4, need to try again. Yay, managed to toss a 6.

* Non devi essere così fortunato. C'è una probabilità del 42% circa di ottenere almeno un sei se lanci tre dadi.

Quell'esempio ci insegna anche qualcosa di più. Vedi come non sono stati effettuati più lanci dopo il primo lancio di successo di un sei? Si noti che tutti i gestori di adempimento (i primi argomenti nelle chiamate a then ) nella catena sono null , tranne l'ultimo, logSuccess . La specifica richiede che se un gestore (adempimento o rifiuto) non è una funzione, la promessa restituita deve essere risolta (soddisfatta o rifiutata) con lo stesso valore. Nell'esempio delle promesse sopra, il gestore dell'adempimento, null , non è una funzione e il valore della promessa è stato soddisfatto con un 6. Quindi anche la promessa restituita dalla chiamata then (la successiva nella catena) verrà soddisfatta con 6 come valore.

Questo si ripete fino a quando non è presente un vero gestore dell'adempimento (uno che è una funzione), quindi l'adempimento si riduce fino a quando non viene gestito. Nel nostro caso, ciò accade alla fine della catena in cui viene allegramente disconnesso dalla console.

Errori di gestione

La specifica Promises/A+ richiede che se una promessa viene rifiutata o viene generato un errore in un gestore di rifiuto, dovrebbe essere gestito da un gestore di rifiuto che è "a valle" dall'origine.

Sfruttare la tecnica del trickle down di seguito offre un modo pulito per gestire gli errori:

 signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)

Poiché un gestore di rifiuti viene aggiunto solo alla fine della catena, se un gestore di evasione ordini nella catena viene rifiutato o genera un errore, scorre fino a quando non incontra displayAndSendErrorReport .

Torniamo ai nostri amati dadi e vediamoli in azione. Supponiamo di voler lanciare i dadi in modo asincrono e stampare i risultati:

 var tossTable = { 1: 'one', 2: 'two', 3: 'three', 4: 'four', 5: 'five', 6: 'six' }; function toss() { return new RSVP.Promise(function(fulfill, reject) { var n = Math.floor(Math.random() * 6) + 1; fulfill(n); }); } function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUppercase() + "."); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain);

Quando lo esegui, non succede nulla. Non viene stampato nulla sulla console e apparentemente non vengono generati errori.

In realtà, viene generato un errore, semplicemente non lo vediamo poiché non ci sono gestori di rifiuto nella catena. Poiché il codice nei gestori viene eseguito in modo asincrono, con un nuovo stack, non viene nemmeno disconnesso dalla console. Risolviamo questo:

 function logAndTossAgain(toss) { var tossWord = tossTable[toss]; console.log("Tossed a " + tossWord.toUpperCase() + "."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } toss() .then(logAndTossAgain) .then(logAndTossAgain) .then(logAndTossAgain) .then(null, logErrorMessage);

L'esecuzione del codice sopra mostra l'errore ora:

 "Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"

Abbiamo dimenticato di restituire qualcosa da logAndTossAgain e la seconda promessa viene soddisfatta con undefined . Il successivo gestore dell'adempimento poi esplode cercando di chiamare toUpperCase su quello. Questa è un'altra cosa importante da ricordare: restituire sempre qualcosa dai gestori o essere preparati nei gestori successivi a non far passare nulla.

Costruire più in alto

Abbiamo ora visto le basi delle promesse JavaScript nel codice di esempio di questo tutorial. Un grande vantaggio nell'usarli è che possono essere composti in modi semplici per produrre promesse "composte" con il comportamento che vorremmo. La libreria rsvp.js fornisce una manciata e puoi sempre crearne di tue usando le primitive e queste di livello superiore.

Per l'ultimo e più complesso esempio, viaggiamo nel mondo dei giochi di ruolo di AD&D e lanciamo i dadi per ottenere i punteggi dei personaggi. Tali punteggi si ottengono tirando tre dadi per ogni abilità del personaggio.

Lascia che incolli prima il codice qui e poi spieghi cosa c'è di nuovo:

 function toss() { var n = Math.floor(Math.random() * 6) + 1; return new RSVP.resolve(n); // [1] } function threeDice() { var tosses = []; function add(x, y) { return x + y; } for (var i=0; i<3; i++) { tosses.push(toss()); } return RSVP.all(tosses).then(function(results) { // [2] return results.reduce(add); // [3] }); } function logResults(result) { console.log("Rolled " + result + " with three dice."); } function logErrorMessage(error) { console.log("Oops: " + error.message); } threeDice() .then(logResults) .then(null, logErrorMessage);

Abbiamo familiarità con il toss dell'ultimo esempio di codice. Crea semplicemente una promessa che viene sempre mantenuta con il risultato del lancio di un dado. Ho usato RSVP.resolve , un metodo conveniente che crea una tale promessa con meno cerimonie (vedi [1] nel codice sopra).

In threeDice , ho creato 3 promesse che rappresentano ciascuna un lancio di dadi e alla fine le ho combinate con RSVP.all . RSVP.all prende una serie di promesse e viene risolta con una serie di valori risolti, uno per ogni promessa costituente, pur mantenendo l'ordine. Ciò significa che abbiamo il risultato dei lanci nei results (vedi [2] nel codice sopra) e restituiamo una promessa che viene soddisfatta con la loro somma (vedi [3] nel codice sopra).

Risolvere la promessa risultante registra quindi il numero totale:

 "Rolled 11 with three dice"

Usare le promesse per risolvere problemi reali

Le promesse JavaScript vengono utilizzate per risolvere problemi in applicazioni che sono molto più complesse dei lanci di dadi asincroni senza motivo .

Se sostituisci tirando tre dadi con l'invio di tre richieste ajax a endpoint separati e procedendo quando tutti sono tornati con successo (o se qualcuno di loro ha fallito), hai già un'utile applicazione di promises e RSVP.all .

Le promesse, se usate correttamente, producono codice di facile lettura su cui è più facile ragionare e quindi più facile da eseguire il debug rispetto ai callback. Non è necessario impostare convenzioni riguardanti, ad esempio, la gestione degli errori poiché fanno già parte della specifica.

Abbiamo appena scalfito la superficie di ciò che le promesse possono fare in questo tutorial JavaScript. Le librerie Promise forniscono una buona dozzina di metodi e costruttori di basso livello a tua disposizione. Padroneggiali e il cielo è il limite in ciò che puoi fare con loro.

Circa l'autore

Balint Erdi era un grande fan dei giochi di ruolo e di AD&D molto tempo fa, ed è una grande promessa e ora è un fan di Ember.js. Ciò che è stata costante è la sua passione per il rock & roll. Ecco perché ha deciso di scrivere un libro su Ember.js che utilizza il rock & roll come tema dell'applicazione nel libro. Iscriviti qui per sapere quando verrà lanciato.