Promisy, callbacky a generátory - asynchronní peklo

Asynchronní neblokující charakter Node.js serveru je dvojsečná zbraň. Na jednu stranu umožňuje psaní výkonných, přesto jednoduchých aplikací, na stranu druhou hází vývojáři klacek pod nohy v podobě nepořádku způsobeném takovým asynchonním kódem.

Začalo to callbackem

Základní knihovny Node.js pro své asynchronní operace používají callback, funkci která se volá poté, co je operace dokončena. Na první pohled je vše jednoduché a čisté:

lib.doSomethingAsync((error, result) => {  
    console.log('Async operation completed');
});

Jenže situace se komplikuje, chceme-li po dokončení předešlé operace provést další operaci, nebo dokonce celou sekvenci operací. Následující příklad mluví za vše a to ještě nijak nezpracovává chyby.

doSomethingAsync((error, result) => {

    doAnotherAsync(result.data, (error, result) => {

        doCoolAsyncOperation(result.data, (error, result) => {

            fancyAsyncMethod(result.data, (error, result) => {
                console.log('All async operation completed');
            });
        });
    });
});

Promise lepší budoucnosti

Existuje však řešení, které dokonce mělo být součástí Node.js, ale bylo od něho upuštěno (je trochu pomalejší a složitější než callback). Promisy se již staly součástí standardu ES6 a v současnosti je podporováno jak Node.js, tak i řadou prohlížečů. Syntaxe je velmi příjemná a dovoluje elegantně ošetřit chyby metodou catch():

doSomethingAsync()  
    .then(() => {
        return doSomethingAsync();
    })
    .then(() => {
        console.log('All async operation completed');
    })
    .catch((err) => {
        console.log('Something failed', err);
    });

Bohužel i toto řešení má své stinné stránky. Vyžaduje psaní spousty "boilerplate" kódu navíc a řada nemá plnou podporu všech modulů. Je pak nutné promisy vytvářet takto:

function doSomethingAsync () {  
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            var result = Math.random();
            resolve(result);
        }, 100);
    });
};

Jak se zbavit zbytečného kódu? Použijme generátory!

Generátory jsou na první pohled úžasný nástroj, jak hvězdičkou a klíčovým slovem yield přeměnit asynchronní kód ve zdálnivě synchrnonní operace. Máme je už k dispozici přímo v Node.js 4.0 a vyšším a nic nám nebrání je použít:

makeAsync(function *() {  
    yield doSomethingAsync();
    yield doSomethingAsync();
    console.log('All async operation completed');
});

Není to ale tak jednoduché. Generátory je funkce, nad kterou je možno iterovat, čímž jsou generovány hodnoty. Iterovat generátor však nemusíme for-on cyklem, ale prostřednictvím jeho metody next() vracející objekt {done: {boolean}, value: {*}}. A pokud bude generovat promisy, můžeme pomocným mechanismem přerušit vykonávání kódu a pokračovat, až bude promise dokončena. Pokud nebudeme řešit eventuelní chyby, může pomocný mechanismus může vypadat takto:

function makeAsync(gen) {  
    const iterator = gen();
    let loop = () => {
        let result = iterator.next();

        if (!result.done) {
            result.value.then(() => {
                loop()
            });
        }
    }
    loop();
};

V praxi je třeba použít sofistikovanější řešení. Výborný modul, který toto řeší, je co a existuje i webserver od tvůrců Express.js, který přímo podporuje generátory - koa.js. Generátory jsou velmi užitečné, pokud potřebujete udělat pořádek v sekvenci asynchronních operací a díky tomu, že existují knihovny podporující promisy jako návratové hodnoty, můžete dosáhnout velmi přehledného kódu. Nevýhoda je však nutnost použít některý z modulů, který generátory podporuje (a obaluje je pomocnou funkcí). Použití try-catch je pak třešníčkou na dortu.

Zajímá vás více o ES6?

A co přímo asynchronní fuknce!

Velmi lákavá vlastnost budoucího standardu ES7, který si můžeme přiblížit například pomocí překladače Babel.js, jsou async / await funkce. Jde nativní implementaci generátorů určenou přímo pro promisy, bohužel si na implementaci v Node.js budeme muset ještě nějaký pátek počkat. Zítřky jsou však zářné:

async function myFunction () {  
    let result = await giveMePromise();
    try {
        await giveMeAnotherPromise(result);
    } catch (e) {
        result = false;
    }
    return result;
}

Jde o velmi pohodlné a čitelné řešení problému s asynchronním kódem, který je velmi očekávanou novou vlastností javascriptu. Leadři javascriptové komunity si to uvědomují, a i proto vzniká například druhá verze moduku Koa.js, která zahazuje generátory a nahrazuje je asynchronními funkcemi.

Podle mého názoru je však používání transpilerů na serverový zdrojový kód přeceňované a často je zisk v podobě nových JS konstrukcí vykoupen hůře laditelným a pomalejším dev-stackem, výjimečně i horším výkonem aplikace. Je na zvážení každého z vás, jestli vám použití takového nástroje skutečně šetří čas.

Pořád to není ono? Zkuste to reaktivně

Pokud však nemáte sekvenční kód, ale potřebujete asynchronní operace větvit a následně spojovat je dobrým řešením reaktivní přístup. Existují knihovny jako kefir.js či bacon.js (kam ty lidi na ty názvy chodí), které vám pomůžou vytvořit pipeline na složitější zpracování dotazů, nebo rovnou toku událostí - event streamu. O tom ale jindy.

David Menger

Read more posts by this author.