Úvod
- JavaScript v browseroch sa vykonáva v jednom vlákne
- ale chceme riešiť veci na pozadí
- príklad: REST API
- nemôžeme čakať, kým dobehne HTTP request na pozadí, trvalo by to dlho, a vlákno by bolo zablokované
- JavaScript nemá analógiu javáckych
Thread
ov, ale má iné mechanizmy: promises
Callbacks
Tradičný spôsob: funkcia má medzi parametrami inú funkciu, callback, ktorú zavolá po dobehnutí.
Callback: príklad setTimeout()
Objekt window
v browseroch má metódu setTimeout()
, ktorá berie:
- obslužnú funkciu
- lehotu, ktorá po vypršaní vyvolá volanie funkcie
Kód:
setTimeout(function() {
console.log("Three seconds elapsed!");
}, 3000);
Alternatívne:
function ding() {
console.log("Ding!")
}
setTimeout(ding, 3000);
Callback: príklad AJAXu cez jQuery
$.getJSON("/deferred-demo/api/entries.json", function(data, textStatus, jqXHR) {
console.log(data);
});
Pyramid of Doom
“Kód ide rýchlejšie doprava než nadol”:
"use strict";
(
function($, Q) {
function deferredTestButtonOnClick() {
$.getJSON("/deferred-demo/api/entries.json", function(data, textStatus, jqXHR) {
data.data.forEach(function(entry) {
var creator = entry.creator;
$.getJSON("/deferred-demo/api/" + creator + ".json", function(data, textStatus, jqXHR) {
console.log(data);
});
});
});
}
$("#deferredTestButton").click(deferredTestButtonOnClick);
}(jQuery, /* Q Promise library */ Q));
Iný príklad: pri reťazení AJAXových volaní sa každé ďalšie volanie rieši ďalším vnoreným callbackom.
Promises
- prísľub budúceho výsledku z asynchrónnej operácie.
- objekty obaľujúce výsledok operácie, ktorý nemusí byť hneď dostupný
Knižnice
Existuje mnoho knižníc:
q.js
when.js
rsvp.js
- jQuery (ale tie sú pokazené!)
Príklad použitia v q.js
var promise = Q.delay(3000);
promise.then(function() {
console.log("Three seconds have elapsed!");
});
- Funkcia
delay()
vracia prísľub budúceho výsledku,promise
. - s premennou
promise
môžeme veselo narábať, aj keď jej výsledok bude dostupný až o tri sekundy- v tomto prípade výsledok nie je žiadny ;-) ukážeme neskôr iné príklady
Špecifikácia Promises/A+
- promise je objekt/funkcia s metódou
then()
, ktorá sa správa podľa špecifikácie Promises/A+. - thenable: objekt/funkcia s metódou
then()
Promise: stavy a prechody
- pending: prechod do fulfilled ALEBO do rejected
- fulfilled: promise je splnený, a nesie v sebe hodnotu, ktorá sa nesmie zmeniť (immutability v zmysle
===
). Stav sa niekde volá aj resolved. - rejected: promise je zamietnutý, a nesie v sebe dôvod zamietnutia (ľubovoľná hodnota), ktorá sa nesmie meniť
Promise bude najprv v stave pending
, potom v stave resolved
.
var promise = Q.delay(3000);
console.log(promise.inspect().state);
promise.then(function() {
console.log("Three seconds have elapsed!");
console.log(promise.inspect().state);
});
Splnený, ani zamietnutý prísľub sa už nikdy nemôže dostať do stavu pending.
Metóda then()
- berie dva parametre, handlery (a.k.a callbacky)
- funkciu
onFulfilled
:- zavolaná po splnení prísľubu
- prvým parametrom je hodnota prísľubu
- funkciu
onRejected
- zavolaná po zamietnutí prísľubu
- prvým parametrom je dôvod zamietnutia
- obe funkcie nebudú mať definovaný
this
(sú to čisté funkcie)
- funkciu
- metódu
then
možno reťaziť:- handlery sa budú volať v poradí registrácie
- vracia nový promise, ktorý je splnený po dobehnutí handlera pre splnenie, či po dobehnutí zamietacieho handlera.
- hodnota z handlera pre splnenie je fulfillment hodnota pre nový promise
Promises z jQuery
Q.js vie obaliť promisy z jQuery, ktoré majú pokazený error handling, a nezodpovedajú špecifikácii Promises/A.
Q($.getJSON("/deferred-demo/api/entries.json"))
.then(function(data) {
console.log(data);
});
$.getJSON()
vracia jQueryácky promise.- konštruktor z
Q
ho obalí do Promise/A+ štandardu - môžeme
then
ovať.
Použitie s dvoma callbackmi
Q($.getJSON("/deferred-demo/api/data.json"))
.then(
function success(data) {
console.log(data);
},
function fail(data) {
console.error(data);
}
);
- ak request zlyhá (napr. na 404), zavolá sa callback
fail()
.
Dva callbacky sú však neprehľadné.
Obsluha chýb
Volanie done()
ukončí reťaz
Ak funkcia nevracia promise, ale len ho spracováva, musíme dodať na koniec reťaze handlerov volanie done()
. V opačnom prípade sa výnimky potichu zhltnú.
Aby sme vedeli reagovať na situáciu, keď AJAXový request zlyhá, zakončíme reťaz
Q($.getJSON("/deferred-demo/api/data.json"))
.then(function(data) {
console.log(data);
})
.done();
Ak sa JSON nepodarí načítať, výnimka prebuble až do obsluhy chýb v okne window
(Chrome zobrazí Uncaught #<Object>
).
Volanie catch()
odchytáva výnimky
Q($.getJSON("/deferred-demo/api/data.json"))
.then(function(data) {
console.log(data);
})
.catch(function(e) {
if(e.status == 404) {
console.error("REST Endpoint not found!");
} else {
console.error(e);
}
})
.done();
Handlery
Poznamenajme, že handler (funkcia, ktorá je parametrom pre then()
) môže vracať tri veci:
- nič
- bežnú hodnotu
- prísľub
- vyhodiť výnimku
Ak handler nevracia nič, alebo vracia nejakú hodnotu, je prísľub vrátený funkciou then
automaticky splnený.
Ak handler vyhodí výnimku, promise je zamietnutý.
Ak handler prísľubu P vracia iný prísľub, prísľub P prevezme stav vráteného prísľubu (prísľub P musí byť pending dovtedy, kým je pending vrátený prísľub, a podobne sa spriahne aj prechod do splneného či zamietnutého vzťahu. Podrobnosti určuje špecifikácia.).
Štýl kódu
Ak sú kroky a obsluhy výnimiek implementované ako funkcie, máme takmer Java/.NET try-catch štýl:
function log(object) {
console.log(object);
}
function handleJsonException(jXHR) {
if(jXHR.status == 404) {
console.error("REST Endpoint not found!");
} else {
console.error(jXHR);
}
}
function deferredTestButtonOnClick() {
Q($.getJSON("/deferred-demo/api/data.json"))
.then(log)
.catch(handleJsonException)
.done();
}
Nezabudneme reťaz ukončiť cez done()
, pretože aj v catch()
by potenciálne mohli nastať chyby či výnimky.
Reťazenie volaní
Vďaka sémantike then
reťaze / rúry volaní a tým písať rozumným spôsobom reťazený asychrónny kód a vyhnúť sa pyramíde záhuby.
Pomocou Q.delay()
simulujme pomalé REST API. V prvom kroku vrátime prísľub pre konštantu 1 po uplynutí 1 sekundy:
function step1() {
return Q.delay(1000)
.then(function() {
return 1;
})
}
V tomto prípade reťaz neukončujeme cez done()
, lebo prísľub vraciame z funkcie.
V druhom kroku simulujeme API, ktoré vie prijať parameter z predošlého volania a vrátiť prísľub jeho desaťnásobku.
function step2(parameter) {
return Q.delay(1000)
.then(function() {
return 10 * parameter;
})
}
V treťom kroku simulujeme API, ktoré vie prijať parameter z predošlého volania a vrátiť prísľub pre jeho zápornú hodnotu.
function step3(parameter) {
return Q.delay(1000)
.then(function() {
return -parameter;
})
}
Keďže všetky funkcie pre kroky vracajú prísľuby (pretože then()
vždy vracia prísľub), môžeme ich zreťaziť:
function deferredTestButtonOnClick() {
step1()
.then(step2)
.then(step3)
.then(function(parameter) {
console.log("Done " + parameter)
})
.done();
}
Optimalizácia kódu
Volanie then
s funkciou, ktorá vracia hodnotu (teda nie promise), možno skrátiť:
function step1() {
return delay(1000)
.thenResolve(1);
}
function step2(parameter) {
return delay(1000)
.thenResolve(10 * parameter);
}
function step3(parameter) {
return delay(1000)
.thenResolve(-parameter);
}
function deferredTestButtonOnClick() {
step1()
.then(step2)
.then(step3)
.then(function(parameter) {
console.log("Done " + parameter)
})
.done();
}
Odovzdávanie hodnôt v reťazi
Samozrejme, funkcie, ktoré vracajú promisy, si môžu cez návratové hodnoty (de facto získavané z thenResolve()
) a parametre odovzdávať ľubovoľné parametre, aj objekty:
function step1() {
return delay(1000)
.thenResolve({ step1 : 1 });
}
function step2(parameter) {
return delay(1000)
.then(function() {
parameter.step2 = 2;
return parameter;
});
}
function step3(parameter) {
return delay(1000)
.then(function() {
parameter.step3 = 3;
return parameter;
});
}
function deferredTestButtonOnClick() {
step1()
.then(step2)
.then(step3)
.then(function(parameter) {
console.log("Done " + JSON.stringify(parameter))
})
.done();
}
Výsledkom na konzole bude:
Done {"step1":1,"step2":2,"step3":3}
Reťazenie v AJAXe
Reťaziť môžeme aj AJAXové volania, samozrejme za predpokladu, že ich obalíme do Q.js prísľubov.
function getEntries() {
return Q($.getJSON("/deferred-demo/api/entries.json"));
}
function getCreator(entries) {
var creator = entries.data[0].creator;
return Q($.getJSON("/deferred-demo/api/" + creator + ".json"));
}
function deferredTestButtonOnClick() {
getEntries()
.then(getCreator)
.then(function(parameter) {
console.log("Done " + JSON.stringify(parameter))
return parameter;
})
.catch(function(failed) {
console.error("Fail " + JSON.stringify(failed));
});
}
Reťazenie volaní 2: try-catch sémantika
Statická metóda Q.try()
obalí ľubovoľnú funkciu a zagarantuje jej asynchrónne vykonanie. Inými slovami, bez ohľadu na to, či výsledok funkcie vráti prísľub alebo pevnú hodnotu, vždy sa vráti prísľub.
Funkcia try()
má aj nestatický variant: na prísľube môžeme zavolať fcall()
.
Toto umožňuje nádherné sekvenčné písanie kódu:
Q.try(getEntries)
.then(getCreator)
.then(logJSON)
.catch(handleJsonException)
.done();
Samozrejme, try
je len syntaktický cukor. Ak sme si istí, že funkcia začínajúca reťaz vracia prísľub, try
môžeme pokojne vynechať.
getEntries()
.then(getCreator)
.then(logJSON)
.catch(handleJsonException)
.done();
Deferred
- manuálne vytvára promises
- jediný účel: premostenie medzi callback-style API a promises-style API
Inými slovami:
- nástroj pre synchronizáciu vlákien
- z deferredu vieme vrátiť prísľub (promise) pre hodnotu.
- vyhodnotením (resolve) deferredu splníme prísľub a dodáme hodnotu
- zamietnutím (reject) deferredu zamietneme príslub a dodáme dôvod
- deferred je škatuľa, v ktorej sa v budúcnosti objaví hodnota. Pošleme ho do druhého vlákna (asynchrónne vykonávaného), ktoré po dobehnutí vloží do deferred bud výsledok operácie (fulfillment hodnotu) alebo dôvod zamietnutia.
- vkladanie beží buď cez
resolve(fulfillment_hodnota)
- alebo zamietanie cez
reject(dôvod_zamietnutia)
- vkladanie beží buď cez
- deferred zverejňuje
promise
budúceho výsledku cez inštančnú premennú
Príklad
Použime deferred na vlastnú implementáciu funkcie delay()
:
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(function() {
deferred.resolve()
}, 1000);
return deferred.promise;
}
- Ručne vyrobíme škatuľu
deferred
, cezQ.defer()
- do
setTimeout
pošleme funkciu, ktorá vyhodnotídeferred
cezresolve()
. Fulfillment hodnotu nepotrebujeme žiadnu, tak ju odignorujeme. (Všimnime si, ako deferred vidieť z dvoch vlákien.) - Z metódy vrátime prísľub budúceho výsledku. V tomto prípade žiaden výsledok nedostaneme, ale vieme reagovať, keď uplynie lehota.
Keďže funkcia delay()
vracia prísľub, vieme naň navešať handlery:
delay(1000)
.then(function() {
console.log("Ding after 1 second");
})
.done();
Upratovanie v kóde
Keďže funkcia setTimeout
po dobehnutí má zavolať funkciu deferred.resolve()
, môžeme si ušetriť zbytočné obalenie funkcie a funkcie spriahnuť priamo. (Funkcia je “objekt”, ktoré môžeme vzájomne priraďovať.)
Pozor, za deferred.resolve
už nie sú guľaté zátvorky, lebo to nie je volanie funkcie!
function delay(milliseconds) {
var deferred = Q.defer();
setTimeout(deferred.resolve, 1000);
return deferred.promise;
}
Samozrejme, handler môže byť funkcia, čo nás povedie k:
function ding() {
console.log("Ding after 1 second");
}
function deferredTestButtonOnClick() {
delay(1000)
.then(ding)
.done();
}
Reťazenie s deferred: logovanie po sekunde
Vytvorme funkciu, ktorá po sekunde vypíše do konzoly hlášku.
function deferLoggingAfterSecond() {
var deferred = Q.defer();
setTimeout(function() {
console.log("Ding!")
deferred.resolve();
}, 1000);
return deferred.promise;
}
Všimnite si, že obslužná funkcia pre setTimeout
nielen vypisuje hlášku, ale aj vyresolvuje deferred.
Ak chceme po každej sekunde vypísať hlášku, môžeme zreťaziť viac prísľubov. Dôležité je, že funkcia deferLoggingAfterSecond()
vracia vždy prísľub!
function deferredTestButtonOnClick() {
Q.try(deferLoggingAfterSecond)
.then(deferLoggingAfterSecond)
.then(deferLoggingAfterSecond)
.done();
}
Kontrast s jQuery
- objekt
Deferred
v jQuery je konceptuálne iný než Q-Deferred.- Q: deferred umožňuje meniť a sledovať stav. Q-Deferred nie je promisom! Na promisy existuje samostatný objekt.
- jQuery:
Deferred
je aj promise, aj deferredom, zverejňuje pohľad na deferred, kde je zakázané meniť stav.
Pozor na deferredy
Neuvedomelé používanie deferredov môže viesť k zbytočne prepletenému kódu, ktorý navyše zle obsluhuje výnimky. Jediný účel je naozaj preklápať callbackové API na promisové.
Q.js má výhodu, že umožňuje preklopiť Node.js callbackové API automaticky, cez metódu denodeify()
.
Záver
- promise uľahčuje zápis asynchrónneho kódu, pretože predstavuje budúcu hodnotu, ktorú si možno pohadzovať ako objekt
- reťaziť promisy môžeme cez
then
- nepoužívajte jQuery promises, zle obsluhujú chyby v reťaziach
- Deferred má špecifické použitie
Pramene
- Promises, Promises (slides)
- You’re Missing the Point of Promises, @domenic, 14. October 2012
- Making Promises with JavaScript
- kriskowal: Q.js
- Q: Getting Started, DocumentUp.com
- Understanding Q-Deferred in NodeJS through example
- Promises/A+ Specification
- Promises/A Specification
- Promises/A+ and Q, José F. Romaniello, 23. May 2013
- Promise Anti-Patterns
- Promises in Node.js with Q: An Alternative to Callbacks, Strongloop.com, 2. July 2013
Velmi pekne zhrnutie! Osobne som zazil callback hell na jednom projekte takze som rad ze mame nieco ako Q!