Asynchrónne veci v JavaScripte cez promises

Ú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 Threadov, 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)
  • 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 thenovať.

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)
  • 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;
}
  1. Ručne vyrobíme škatuľu deferred, cez Q.defer()
  2. do setTimeout pošleme funkciu, ktorá vyhodnotí deferred cez resolve(). Fulfillment hodnotu nepotrebujeme žiadnu, tak ju odignorujeme. (Všimnime si, ako deferred vidieť z dvoch vlákien.)
  3. 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

One thought on “Asynchrónne veci v JavaScripte cez promises

Pridaj komentár

Vaša e-mailová adresa nebude zverejnená. Vyžadované polia sú označené *