Promise Pattern – asynchronní programování v JavaScriptu

„Už tam budem? – Ne! – Už tam budem? – Ještě ne. – Tak už tam budem? – Ne. – Už tam budem? – Ne. – Tak už tam budeeeeeeem?“ No, tak přesně tahle asychronní programování nevypadá.
Vezměte si, že máte nějakou časově náročnější akci, po jejímž dokončení potřebujete něco provést. Například AJAX request, zpracování odpovědi a zobrazení výsledku. Nejdříve si ukážeme, jak to nedělat. Následovat bude několik použitelnějších přístupů.
Jak je to špatně (synchronně)
try { var response = doSomeRequest(), // 5 vteřin čekáme na výsledek requestu result = processResponse(response); // složité zpracování – další 2 vteřiny 🙂 showResult(result); } catch (error) { handleError(error); }
Proč bychom měli čekat 7 vteřin na dokončení, když mezi tím může skript běžet dál? Navíc se nám bude kód zbytečně hemžit odchytáváním výjimek při každém requestu. Fuj.
A jak je to lepší?
Jak jsem zmínil na začátku, existují samozřejmě hezčí řešení. Řeč je o callbacku, události a promise.
Callback
Callback je jednoduše řečeno funkce / metoda, která se zavolá po dokončení operace. Typicky se jí také předá výsledek operace, na kterou je navázána. Náš příklad by tedy mohl vypadat následovně.
doSomeRequest(function (response) { processResponse(response, showResult); }, handleErrors);
Po dokončení requestu se zavolá předaný callback, který zpracuje
odpověď a opět jako callback zavolá funkci showResult
.
V případě neúspěchu doSomeRequest
se zavolá funkce, která
chybu zpracuje.
Kód sice vypadá rozumně, ale má několik nevýhod. Callback je vždy jen jeden a na jednu akci jich nemůžeme navázat více. Například kdybychom chtěli se zpracovaným výsledkem provést ještě nějakou další akci, nezbývá nám než změnit stávající callback.
doSomeRequest(function (response) { processResponse(response, function (result) { showResult(result); doSomeMagic(result); }); }, handleErrors);
Na tom si můžeme všimnout další nevýhody, a to neustálého zanořování callbacků. Skládáme si tak podivnou JavaScriptovou matrjošku. Nutno podotknout, že ve valné většině případů je obyčejný callback naprosto dostačující, transparentní a není potřeba ho vyměnit za cokoli jiného.
Event
Události jsou další v JavaScriptu velmi oblíbenou technikou. Při vytváření UI je s nimi člověk v neustálém kontaktu. Řešit například obsluhu tlačítka jinak, než pomocí události, je snad i holý nesmysl. Pro náš příklad se události zase tolik nehodí, ale pokud bychom je chtěli využít, mohl by kód vypadat například takto.
doSomeRequest(function (response) { var responseProcessor = new ResponseProcessor(); responseProcessor.addEventListener('processed', showResult); responseProcessor.addEventListener('processed', doSomeMagic); responseProcessor.process(response); }, handleErrors);
Tím na událost „processed“ navážeme dva posluchače, jejichž kód je na základě vyvolání události vykonán.
Hlavními problémy jsou de facto nemožnost nějak synchronizovat akce
navěšené na události – v ideálním stavu by se dokonce měly zavolat
všechny najednou okamžitě po vyvolání události. Další nepříjemností
je, že vlastně neexistuje žádné standardní API. V DOMu se používají
metody addEventListener(eventType, callback)
, v jQuery jsou metody
pojmenovány podle událostí $('a.button').click(callback)
,
Prototype používá metody observe(eventType, callback)
atd.
Promise
Konečně se okružní jízdou dostáváme k tématu z názvu článku. Promise je pattern, který nám umožňuje programovat asynchronně, určovat pořadí, v jakém jsou obslužné callbacky prováděny, a dokonce definovat více podmínek, které musí být současně splněny pro spuštění callbacků.
Promise má poměrně pevně dané API (pozn.: místo metody
resolve
lze občas zahlédnout fulfill
).
var Promise = function () { // základní inicializace }; Promise.prototype.then = function (onResolved, onRejected) { // zavolání callbacku na základě stavu }; Promise.prototype.resolve = function (value) { // změna stavu z „nesplněný“ na resolved }; Promise.prototype.reject = function (error) { // změna stavu z „nesplněný“ na rejected };
Náš příklad, který pouze zobrazuje výsledek by s použitím Promise tedy vypadal takto.
doSomeRequest() .then(processResponse, handleErrors) .then(showResult);
Moc pěkné, není-liž pravda? 🙂 Metoda doSomeRequest
v tomto případě vypadá zhruba takto.
var doSomeRequest = function () { var url, xhr, results, promise; promise = new Promise(); url = ... xhr = ... xhr.open(...); xhr.onload = function (e) { if(this.status === 200) { result = JSON.parse(this.responseText); promise.resolve(result); } }; xhr.onerror = function (e) { promise.reject(e); }; return promise; };
Zde můžeme vidět v akci jak „splnění slibu“, tak jeho
„odmítnutí“. Základním principem je, že metoda
doSomeRequest
proběhne celá a vrátí objekt
promise
. Vykonávání skriptu běží dál a až při zavolání
promise.resolve()
se spustí zpracování odpovědi.
V Promise většinou nalezneme ještě metodu when
,
používanou pro spojení více Promise dohromady. Obslužné callbacky se tedy
provedou až po splnění všech Promises.
(new Promise() .when(doSomeRequest()) .when(doSomeOtherRequest()) .then(processResponses, handleErrors) .then(showResults));
Nyní se metoda processResponses
zavolá až splnění obou
requestů.
To by bylo o Promise patternu z mé strany vše. Doufám, že vás zaujal. Klidně se o své postřehy podělte v komentářích.
Více se můžete dočíst ze zdrojů, ze kterých jsem čerpal: Matt Podwysocki, Amanda Silver – Asynchronous Programming in JavaScript with “Promises”, Sebastiaan Deckers – Promise pattern for asynchronous JavaScript.
-
Petr Jirásek
-
Vojtěch Bartoš
-
Patrik Šíma
-
Taco