JavaScript Promises: Un tutorial cu exemple
Publicat: 2022-03-11Promisiunile sunt un subiect fierbinte în cercurile de dezvoltare JavaScript și cu siguranță ar trebui să vă familiarizați cu ele. Nu sunt ușor de înfășurat capul; poate fi nevoie de câteva tutoriale, exemple și o cantitate decentă de practică pentru a le înțelege.
Scopul meu cu acest tutorial este să vă ajut să înțelegeți promisiunile JavaScript și să vă îndemn să exersați să le folosiți mai mult. Voi explica ce sunt promisiunile, ce probleme rezolvă și cum funcționează. Fiecare pas, descris în acest articol, este însoțit de un exemplu de cod jsbin
pentru a vă ajuta să lucrați împreună și pentru a fi folosit ca bază pentru explorare ulterioară.
Ce este o promisiune JavaScript?
O promisiune este o metodă care în cele din urmă produce o valoare. Poate fi considerat ca omologul asincron al unei funcții getter. Esența sa poate fi explicată astfel:
promise.then(function(value) { // Do something with the 'value' });
Promisiunile pot înlocui utilizarea asincronă a apelurilor inverse și oferă mai multe beneficii față de acestea. Încep să câștige teren pe măsură ce din ce în ce mai multe biblioteci și cadre le îmbrățișează ca modalitate principală de a gestiona asincronismul. Ember.js este un exemplu grozav al unui astfel de cadru.
Există mai multe biblioteci care implementează specificația Promises/A+. Vom învăța vocabularul de bază și vom lucra prin câteva exemple de promisiuni JavaScript pentru a introduce conceptele din spatele lor într-un mod practic. Voi folosi una dintre cele mai populare biblioteci de implementare, rsvp.js, în exemplele de cod.
Pregătește-te, vom arunca o mulțime de zaruri!
Obținerea bibliotecii rsvp.js
Promises și, prin urmare, rsvp.js, pot fi folosite atât pe server, cât și pe partea client. Pentru a-l instala pentru nodejs , accesați folderul proiectului și tastați:
npm install --save rsvp
Dacă lucrezi pe front-end și folosești bower, este doar un
bower install -S rsvp
departe.
Dacă doriți doar să intrați direct în joc, îl puteți include printr-o etichetă de script simplă (și cu jsbin
, îl puteți adăuga prin meniul drop-down „Adăugați o bibliotecă”):
<script src="//cdn.jsdelivr.net/rsvp/3.0.6/rsvp.js"></script>
Ce proprietăți are o promisiune?
O promisiune poate fi în una dintre cele trei stări: în așteptare , îndeplinită sau respinsă . Când este creată, promisiunea este în stare de așteptare. De aici, poate trece fie în starea îndeplinită, fie în starea respinsă. Numim această tranziție rezoluția promisiunii . Starea rezolvată a unei promisiuni este starea sa finală, așa că odată ce este îndeplinită sau respinsă, rămâne acolo.
Modul de a crea o promisiune în rsvp.js este prin ceea ce se numește un constructor revelator. Acest tip de constructor ia un singur parametru de funcție și îl apelează imediat cu două argumente, fulfill
și reject
, care pot trece promisiunea fie la starea fulfilled
, fie la starea rejected
:
var promise = new RSVP.Promise(function(fulfill, reject) { (...) });
Acest model de promisiuni JavaScript este numit un constructor revelator, deoarece argumentul cu o singură funcție își dezvăluie capacitățile funcției de constructor, dar asigură că consumatorii promisiunii nu pot manipula starea acesteia.
Consumatorii promisiunii pot reacționa la modificările stării acesteia adăugând handlerul lor prin metoda then
. Este nevoie de o funcție de gestionare a îndeplinirii și a respingerii, ambele pot lipsi.
promise.then(onFulfilled, onRejected);
În funcție de rezultatul procesului de rezolvare a promisiunii, fie onFulfilled
, fie onRejected
este apelat asincron .
Să vedem un exemplu care arată în ce ordine sunt executate lucrurile:
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');
Acest fragment imprimă rezultate similare cu următoarea:
1 2 3 Oh, noes, threw a 4.
Sau, dacă avem noroc, vedem:
1 2 3 Yay, threw a 6.
Acest tutorial promite demonstrează două lucruri.
În primul rând, că handlerii pe care i-am atașat promisiunii au fost într-adevăr apelați după ce toate celelalte coduri au rulat, în mod asincron.
În al doilea rând, că fulfilment handler a fost chemat doar atunci când promisiunea a fost îndeplinită, cu valoarea cu care s-a rezolvat (în cazul nostru, rezultatul tragerii cu zarurile). Același lucru este valabil și pentru gestionarea respingerii.
Înlănțuind promisiunile și prelingând-o
Specificația necesită ca funcția then
(de gestionare) să returneze și o promisiune, ceea ce permite înlănțuirea promisiunilor împreună, rezultând un cod care pare aproape sincron:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage)
Aici, signupPayingUser
returnează o promisiune și fiecare funcție din lanțul de promisiuni este apelată cu valoarea de returnare a handler-ului anterior odată ce a fost finalizată. Pentru toate scopurile practice, aceasta serializează apelurile fără a bloca firul de execuție principal.
Pentru a vedea cum se rezolvă fiecare promisiune cu valoarea returnată a elementului anterior din lanț, revenim la aruncarea zarurilor. Vrem să aruncăm zarurile de maximum trei ori sau până când primele șase apar 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
Când rulați acest exemplu de cod de promisiuni, veți vedea ceva de genul acesta pe consolă:
Tossed a 2, need to try again. Tossed a 1, need to try again. Tossed a 4. Too bad, couldn't roll a six.
Promisiunea returnată de tossASix
este respinsă atunci când aruncarea nu este un șase, așa că responsabilul de respingere este chemat cu aruncarea efectivă. logAndTossAgain
afișează rezultatul pe consolă și returnează o promisiune care reprezintă o altă aruncare a zarurilor. La rândul său, această aruncare este respinsă și deconectată de următorul logAndTossAgain
.
Uneori, însă, ai noroc* și reușești să dai un șase:

Tossed a 4, need to try again. Yay, managed to toss a 6.
* Nu trebuie să fii atât de norocos. Există o șansă de ~42% să aruncați cel puțin șase dacă aruncați trei zaruri.
Acest exemplu ne învață și ceva mai mult. Vedeți cum nu s-au mai făcut aruncări după prima aruncare cu succes a unui șase? Rețineți că toți manipulatorii de îndeplinire (primele argumente din apelurile la then
) din lanț sunt null
, cu excepția ultimului, logSuccess
. Specificația cere ca, dacă un handler (îndeplinire sau respingere) nu este o funcție, atunci promisiunea returnată trebuie rezolvată (îndeplinată sau respinsă) cu aceeași valoare. În exemplul de promisiuni de mai sus, handlerul de îndeplinire, null
, nu este o funcție și valoarea promisiunii a fost îndeplinită cu un 6. Deci, promisiunea returnată de apelul de then
(următorul din lanț) va fi, de asemenea, îndeplinită. cu 6 ca valoare.
Acest lucru se repetă până când este prezent un handler real de îndeplinire (unul care este o funcție), astfel încât îndeplinirea se scurge până când este gestionată. În cazul nostru, acest lucru se întâmplă la sfârșitul lanțului, unde este deconectat cu bucurie de pe consolă.
Manevrarea erorilor
Specificația Promises/A+ cere ca dacă o promisiune este respinsă sau o eroare este aruncată într-un handler de respingere, aceasta ar trebui să fie gestionată de un handler de respingere care este „în aval” de la sursă.
Folosirea tehnicii de scurgere de mai jos oferă o modalitate curată de a gestiona erorile:
signupPayingUser .then(displayHoorayMessage) .then(queueWelcomeEmail) .then(queueHandwrittenPostcard) .then(redirectToThankYouPage) .then(null, displayAndSendErrorReport)
Deoarece un handler de respingere este adăugat doar la sfârșitul lanțului, dacă orice handler de îndeplinire din lanț este respins sau aruncă o eroare, acesta se scurge până când se lovește displayAndSendErrorReport
.
Să ne întoarcem la zarurile noastre iubite și să vedem asta în acțiune. Să presupunem că vrem doar să aruncăm zarurile asincron și să imprimăm rezultatele:
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);
Când rulezi asta, nu se întâmplă nimic. Nimic nu este tipărit pe consolă și nu se aruncă erori, aparent.
În realitate, o eroare este aruncată, pur și simplu nu o vedem, deoarece nu există gestionari de respingere în lanț. Deoarece codul din handlere este executat asincron, cu o stivă nouă, nici măcar nu este deconectat de la consolă. Să reparăm asta:
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);
Rularea codului de mai sus arată acum eroarea:
"Tossed a TWO." "Oops: Cannot read property 'toUpperCase' of undefined"
Am uitat să returnăm ceva de la logAndTossAgain
și a doua promisiune este îndeplinită cu undefined
. Următorul handler de îndeplinire explodă apoi încercând să sune la toUpperCase
. Acesta este un alt lucru important de reținut: returnați întotdeauna ceva de la handleri sau fiți pregătit în manipulatorii ulterioare să nu treacă nimic.
Construire mai sus
Am văzut acum elementele de bază ale promisiunilor JavaScript în exemplul de cod al acestui tutorial. Un mare beneficiu al utilizării lor este că pot fi compuse în moduri simple pentru a produce promisiuni „compuse” cu comportamentul pe care ni l-am dori. Biblioteca rsvp.js
oferă o mână de ele și vă puteți crea oricând propria dvs. folosind primitivele și acestea de nivel superior.
Pentru exemplul final, cel mai complex, călătorim în lumea jocului de rol AD&D și aruncăm zaruri pentru a obține scoruri ale personajelor. Astfel de scoruri se obțin prin aruncarea a trei zaruri pentru fiecare abilitate a personajului.
Lasă-mă să lipesc mai întâi codul aici și apoi să explic ce este nou:
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);
Suntem familiarizați cu toss
din ultimul exemplu de cod. Pur și simplu creează o promisiune care este întotdeauna îndeplinită cu rezultatul aruncării unui zar. Am folosit RSVP.resolve
, o metodă convenabilă care creează o astfel de promisiune cu mai puțină ceremonie (vezi [1] în codul de mai sus).
În threeDice
, am creat 3 promisiuni care reprezintă fiecare o aruncare cu zaruri și, în final, le-am combinat cu RSVP.all
. RSVP.all
preia o serie de promisiuni și este rezolvată cu o serie de valori rezolvate, câte una pentru fiecare promisiune constitutivă, menținându-și ordinea. Asta înseamnă că avem rezultatul aruncărilor în results
(vezi [2] în codul de mai sus) și returnăm o promisiune care este îndeplinită cu suma lor (vezi [3] în codul de mai sus).
Rezolvarea promisiunii rezultate, apoi înregistrează numărul total:
"Rolled 11 with three dice"
Folosind promisiuni pentru a rezolva probleme reale
Promisiunile JavaScript sunt folosite pentru a rezolva probleme în aplicații care sunt mult mai complexe decât aruncările de zaruri asincrone fără un motiv întemeiat .
Dacă înlocuiți aruncarea a trei zaruri cu trimiterea a trei solicitări ajax către puncte finale separate și continuarea când toate au revenit cu succes (sau dacă oricare dintre ele a eșuat), aveți deja o aplicație utilă a promisiunilor și RSVP.all
.
Promisele, atunci când sunt folosite corect, produc cod ușor de citit, care este mai ușor de raționat și, prin urmare, mai ușor de depanat decât apelurile inverse. Nu este nevoie să se stabilească convenții privind, de exemplu, tratarea erorilor, deoarece acestea fac deja parte din specificație.
Abia am zgâriat suprafața a ceea ce promisiunile pot face în acest tutorial JavaScript. Bibliotecile Promise oferă o duzină bună de metode și constructori de nivel scăzut care vă stau la dispoziție. Stăpânește-le și cerul este limita în ceea ce poți face cu ele.
Despre autor
Balint Erdi a fost un mare fan al jocurilor de rol și al AD&D cu mult timp în urmă și este o mare promisiune și fan Ember.js acum. Ceea ce a fost constant este pasiunea lui pentru rock & roll. De aceea a decis să scrie o carte pe Ember.js care folosește rock & roll ca temă a aplicației din carte. Înscrieți-vă aici pentru a afla când se lansează.