Nepříliš chytrý blog Od programování po jezevce

15Čvc/124
Javascript

Promise Pattern – asynchronní programování v JavaScriptu

i_promise

„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.

  • Hezký článek

  • Super clanek, ale jeste by stalo za to ukazat simple implementaci.
    Je dobry mit poneti o cem to je ale taky jak to presneji uvnitr funguje :)

    Jinak super ;)

  • Tuhle chybu, tedy synchronní posloupnost příkazů v asynchronním programování vídám docela často. A bohužel to při lokálním testování často funguje, a jen občas spadne. Což je nejhorší druh chyb.

  • Moc zajímavé. Základní princip asi chápu. Jen mě nedocvakává to druhé volání
    [code]
    .then(showResult);
    [/code]
    Ledaže by then() vracelo novou instanci Promise. Je to tak?