Přechod na asynchronní s generátory ES6

Generátory ES6:Kompletní série

  1. Základy generátorů ES6
  2. Hlubší potápění s generátory ES6
  3. Asynchronizace s generátory ES6
  4. Souběh s generátory ES6

Nyní, když jste viděli generátory ES6 a jste s nimi pohodlnější, je čas je skutečně použít ke zlepšení našeho reálného kódu.

Hlavní předností generátorů je, že poskytují styl kódu s jedním vláknem, synchronně vyhlížející, a přitom vám umožňují skrýt asynchronitu jako detail implementace . To nám umožňuje vyjádřit velmi přirozeným způsobem, jaký je tok kroků/příkazů našeho programu, aniž bychom museli současně procházet asynchronní syntaxí a gotchami.

Jinými slovy, dosáhneme pěkného oddělení schopností/obav , rozdělením spotřeby hodnot (naše generátorová logika) od detailu implementace asynchronního plnění těchto hodnot (next(..) iterátoru generátoru).

Výsledek? Veškerá síla asynchronního kódu, s veškerou snadností čtení a udržovatelností synchronního (vypadajícího) kódu.

Jak tedy tento výkon dosáhneme?

Nejjednodušší asynchronní

Ve své nejjednodušší podobě generátory nepotřebují nic navíc pro zpracování asynchronních funkcí, které váš program ještě nemá.

Předpokládejme například, že tento kód již máte:

function makeAjaxCall(url,cb) {
    // do some ajax fun
    // call `cb(result)` when complete
}

makeAjaxCall( "http://some.url.1", function(result1){
    var data = JSON.parse( result1 );

    makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
        var resp = JSON.parse( result2 );
        console.log( "The value you asked for: " + resp.value );
    });
} );

Chcete-li použít generátor (bez jakékoli další dekorace) k vyjádření stejného programu, postupujte takto:

function request(url) {
    // this is where we're hiding the asynchronicity,
    // away from the main code of our generator
    // `it.next(..)` is the generator's iterator-resume
    // call
    makeAjaxCall( url, function(response){
        it.next( response );
    } );
    // Note: nothing returned here!
}

function *main() {
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
}

var it = main();
it.next(); // get it all started

Pojďme se podívat, jak to funguje.

request(..) helper v podstatě zabalí náš normální makeAjaxCall(..) obslužný program, aby se ujistil, že jeho zpětné volání vyvolá next(..) iterátoru generátoru metoda.

Pomocí request("..") zavolejte, všimnete si, že nemá žádnou návratovou hodnotu (jinými slovy je to undefined ). To není velký problém, ale je to něco důležitého v kontrastu s tím, jak k věcem přistupujeme dále v tomto článku:efektivně yield undefined zde.

Potom zavoláme yield .. (s tím undefined value), který v podstatě nedělá nic jiného, ​​než že v tom okamžiku náš generátor pozastaví. Bude to čekat do it.next(..) hovor se obnoví, což jsme zařadili do fronty (jako zpětné volání), aby se uskutečnilo po dokončení našeho hovoru Ajax.

Co se ale stane s výsledkem ze yield .. výraz? To přiřadíme proměnné result1 . Jak je v tom výsledek prvního volání Ajaxu?

Protože když it.next(..) se nazývá zpětné volání Ajaxu, předává mu odpověď Ajax, což znamená, že hodnota je odesílána zpět do našeho generátoru v bodě, kde je aktuálně pozastavena, což je uprostřed result1 = yield .. prohlášení!

To je opravdu skvělé a super výkonné. V podstatě result1 = yield request(..) žádá o hodnotu , ale je nám (téměř!) zcela skryto – alespoň my se tím zde nemusíme zabývat – že implementace pod krytem způsobuje, že tento krok je asynchronní. Dosahuje této asynchronicity skrytím pauzy schopnost v yield a oddělením životopisu schopnost generátoru na jinou funkci, takže náš hlavní kód pouze vytváří synchronní (vypadající) požadavek na hodnotu .

Totéž platí pro druhý result2 = yield result(..) prohlášení:transparentně se pozastavuje a obnovuje a poskytuje nám hodnotu, o kterou jsme žádali, a to vše, aniž by nás obtěžovalo detaily asynchronicity v tomto bodě našeho kódování.

Samozřejmě, yield je přítomen, takže je jemný náznak toho, že se může objevit něco magického (aka asynchronního). v tom bodě. Ale yield je docela malý syntaktický signál/režie ve srovnání s pekelnými nočními můrami vnořených zpětných volání (nebo dokonce režií API řetězců slibů!).

Všimněte si také, že jsem řekl „může nastat“. To je samo o sobě dost silná věc. Výše uvedený program vždy provede asynchronní volání Ajax, ale co když ne? Co kdybychom později změnili náš program tak, aby měl v paměti mezipaměť předchozích (nebo předem načtených) odpovědí Ajax? Nebo nějaká jiná složitost směrovače URL naší aplikace může v některých případech splnit požadavek Ajax okamžitě , aniž byste jej museli skutečně načítat ze serveru?

Mohli bychom změnit implementaci request(..) na něco takového:

var cache = {};

function request(url) {
    if (cache[url]) {
        // "defer" cached response long enough for current
        // execution thread to complete
        setTimeout( function(){
            it.next( cache[url] );
        }, 0 );
    }
    else {
        makeAjaxCall( url, function(resp){
            cache[url] = resp;
            it.next( resp );
        } );
    }
}

Poznámka: Jemným a záludným detailem je potřeba setTimeout(..0) odložení v případě, že cache již má výsledek. Kdybychom právě zavolali it.next(..) okamžitě by to způsobilo chybu, protože (a to je ta záludná část) generátor není technicky zatím v pozastaveném stavu . Volání naší funkce request(..) nejprve se plně vyhodnocuje a poté yield pauzy. Nemůžeme tedy volat it.next(..) znovu zatím bezprostředně uvnitř request(..) , protože přesně v tu chvíli generátor stále běží (yield nebyl zpracován). Ale můžeme volejte it.next(..) "později", ihned po dokončení aktuálního vlákna, což je naše setTimeout(..0) "hack" dosáhne. Na to budeme mít mnohem hezčí odpověď níže.

Nyní náš hlavní generátorový kód stále vypadá takto:

var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..

Vidíte!? Naše logika generátoru (také naše řízení toku ) se nemuselo vůbec měnit z výše uvedené verze bez podpory mezipaměti.

Kód v *main() stále jen požádá o hodnotu a pozastaví dokud to nedostane zpět, než půjde dál. V našem současném scénáři by tato "pauza" mohla být relativně dlouhá (vytvoření skutečného požadavku serveru, možná 300-800 ms) nebo by mohla být téměř okamžitá (setTimeout(..0) odložení hacku). Ale naší kontrole toku je to jedno.

To je skutečná síla abstrakce asynchronicity jako detailu implementace.

Lepší asynchronní

Výše uvedený přístup je docela vhodný pro práci s jednoduchými asynchronními generátory. Ale rychle se to stane omezujícím, takže budeme potřebovat výkonnější asynchronní mechanismus, který by se dal spárovat s našimi generátory, který je schopen zvládnout mnohem více těžkého zvedání. Ten mechanismus? Sliby .

Pokud jste stále trochu zmatení ohledně ES6 Promises, napsal jsem o nich rozsáhlou 5dílnou sérii blogových příspěvků. Jdi si přečíst. Počkám aby ses vrátil. . Jemné, banální asynchronní vtipy ftw!

Dřívější příklady kódu Ajax zde trpí všemi stejnými problémy s inverzí ovládání (aka „peklo zpětného volání“) jako náš počáteční příklad vnořeného zpětného volání. Pár postřehů, kde nám věci zatím chybí:

  1. Neexistuje žádná jasná cesta pro zpracování chyb. Jak jsme se dozvěděli v předchozím příspěvku, mohli detekovali chybu ve volání Ajax (nějak), předali ji zpět našemu generátoru s it.throw(..) a poté použili try..catch v naší logice generátoru, abychom to zvládli. Ale to je jen další ruční práce na zapojení do "back-endu" (kódu obsluhujícího náš iterátor generátoru) a nemusí to být kód, který bychom mohli znovu použít, pokud v našem programu děláme mnoho generátorů.
  2. Pokud makeAjaxCall(..) utilita není pod naší kontrolou a stane se, že volá zpětné volání vícekrát nebo signalizuje úspěch i chybu současně atd., pak se náš generátor zhroutí (nezachycené chyby, neočekávané hodnoty atd.). Řešení a prevence takových problémů je spousta opakující se ruční práce, která také možná není přenosná.
  3. Často musíme udělat více než jeden úkol „paralelně“ (například dvě současná volání Ajaxu). Od generátoru yield každý příkaz je jedním bodem pauzy, dva nebo více nelze spustit současně - musí se spustit po jednom, v daném pořadí. Není tedy příliš jasné, jak odpálit více úloh v jediném generátoru yield bez zapojení velkého množství ručního kódu pod kryty.

Jak vidíte, všechny tyto problémy jsou řešitelné , ale kdo opravdu chce tato řešení pokaždé znovu vymýšlet. Potřebujeme výkonnější vzor, ​​který je navržen speciálně jako důvěryhodné, opakovaně použitelné řešení pro naše asynchronní kódování založené na generátoru.

Ten vzor? yield plnění slibů a nechat je obnovit generátor, když splní.

Připomeňme výše, že jsme provedli yield request(..) a že request(..) obslužný program neměl žádnou návratovou hodnotu, takže byl ve skutečnosti jen yield undefined ?

Pojďme to trochu upravit. Změňme naše request(..) nástroj musí být založen na slibech, takže vrátí slib, a tedy to, co yield out je vlastně příslib (a ne undefined ).

function request(url) {
    // Note: returning a promise now!
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } );
}

request(..) nyní vytváří příslib, který bude vyřešen, až volání Ajaxu skončí, a my tento příslib vrátíme, takže může být yield ed out. Co dál?

Budeme potřebovat nástroj, který řídí iterátor našeho generátoru, který bude přijímat tyto yield ed slibuje a zapojí je, aby obnovil generátor (přes next(..) ). Tento nástroj budu nazývat runGenerator(..) prozatím:

// run (async) a generator to completion
// Note: simplified approach: no error handling here
function runGenerator(g) {
    var it = g(), ret;

    // asynchronously iterate over generator
    (function iterate(val){
        ret = it.next( val );

        if (!ret.done) {
            // poor man's "is it a promise?" test
            if ("then" in ret.value) {
                // wait on the promise
                ret.value.then( iterate );
            }
            // immediate value: just send right back in
            else {
                // avoid synchronous recursion
                setTimeout( function(){
                    iterate( ret.value );
                }, 0 );
            }
        }
    })();
}

Klíčové věci, kterých si musíte všimnout:

  1. Automaticky inicializujeme generátor (vytvoříme jeho it iterátor) a asynchronně spustíme it do dokončení (done:true ).
  2. Očekáváme, že příslib bude yield ed out (neboli návrat value z každého it.next(..) volání). Pokud ano, počkáme na dokončení registrací then(..) na slib.
  3. Pokud je vrácena jakákoli okamžitá (neboli neslíbená) hodnota, jednoduše tuto hodnotu pošleme zpět do generátoru, takže bude okamžitě pokračovat.

Jak to teď použijeme?

runGenerator( function *main(){
    var result1 = yield request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = yield request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );

Bam! Počkejte... to je přesně stejný kód generátoru jako dříve ? Ano. Opět je to síla generátorů, která se předvádí. Skutečnost, že nyní vytváříme sliby, yield jejich vyjmutí a obnovení generátoru po jejich dokončení -- VŠECHNY TOTO JSOU "SKRYTÉ" PODROBNOSTI IMPLEMENTACE! Ve skutečnosti to není skryté, je to jen oddělené od kódu spotřeby (naše řízení průtoku v našem generátoru).

Čekáním na yield vypíše slib a poté odešle jeho hodnotu dokončení zpět do it.next(..) , result1 = yield request(..) získá hodnotu přesně jako předtím.

Ale teď, když používáme přísliby pro správu asynchronní části kódu generátoru, řešíme všechny problémy s inverzí/důvěrou z přístupů kódování pouze se zpětným voláním. Všechna tato řešení našich výše uvedených problémů získáváme „zdarma“ pomocí generátorů + slibů:

  1. Nyní máme vestavěné zpracování chyb, které lze snadno zapojit. Výše jsme to v našem runGenerator(..) neukázali , ale není vůbec těžké poslouchat chyby ze slibu a poslat je na it.throw(..) -- pak můžeme použít try..catch v našem generátorovém kódu k zachycení a zpracování chyb.
  2. Dostáváme veškerou kontrolu/důvěryhodnost, kterou sliby nabízejí. Žádné starosti, žádné starosti.
  3. Promises mají nad sebou spoustu výkonných abstrakcí, které automaticky zvládají složitosti více „paralelních“ úkolů atd.

    Například yield Promise.all([ .. ]) by vyžadovalo řadu slibů pro "paralelní" úkoly a yield vydá jeden příslib (pro zpracování generátorem), který před pokračováním čeká na dokončení všech dílčích příslibů (v libovolném pořadí). Co byste dostali zpět z yield výraz (když příslib skončí) je pole všech odpovědí na dílčí příslib v pořadí, v jakém byly požadovány (je tedy předvídatelný bez ohledu na pořadí dokončení).

Nejprve prozkoumáme zpracování chyb:

// assume: `makeAjaxCall(..)` now expects an "error-first style" callback (omitted for brevity)
// assume: `runGenerator(..)` now also handles error handling (omitted for brevity)

function request(url) {
    return new Promise( function(resolve,reject){
        // pass an error-first style callback
        makeAjaxCall( url, function(err,text){
            if (err) reject( err );
            else resolve( text );
        } );
    } );
}

runGenerator( function *main(){
    try {
        var result1 = yield request( "http://some.url.1" );
    }
    catch (err) {
        console.log( "Error: " + err );
        return;
    }
    var data = JSON.parse( result1 );

    try {
        var result2 = yield request( "http://some.url.2?id=" + data.id );
    } catch (err) {
        console.log( "Error: " + err );
        return;
    }
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
} );

Pokud dojde k odmítnutí slibu (nebo k jinému druhu chyby/výjimky) během načítání adresy URL, bude odmítnutí slibu namapováno na chybu generátoru (pomocí -- nezobrazeno -- it.throw(..) v runGenerator(..) ), který bude zachycen kódem try..catch prohlášení.

Nyní se podívejme na složitější příklad, který používá sliby pro správu ještě větší asynchronní složitosti:

function request(url) {
    return new Promise( function(resolve,reject){
        makeAjaxCall( url, resolve );
    } )
    // do some post-processing on the returned text
    .then( function(text){
        // did we just get a (redirect) URL back?
        if (/^https?:\/\/.+/.test( text )) {
            // make another sub-request to the new URL
            return request( text );
        }
        // otherwise, assume text is what we expected to get back
        else {
            return text;
        }
    } );
}

runGenerator( function *main(){
    var search_terms = yield Promise.all( [
        request( "http://some.url.1" ),
        request( "http://some.url.2" ),
        request( "http://some.url.3" )
    ] );

    var search_results = yield request(
        "http://some.url.4?search=" + search_terms.join( "+" )
    );
    var resp = JSON.parse( search_results );

    console.log( "Search results: " + resp.value );
} );

Promise.all([ .. ]) vytváří slib, který čeká na tři dílčí sliby, a je to ten hlavní slib, který je yield ed out pro runGenerator(..) nástroj k poslechu pro obnovení generátoru. Dílčí přísliby mohou obdržet odpověď, která vypadá jako další adresa URL, na kterou lze přesměrovat, a zřetězit další příslib dílčí žádosti do nového umístění. Chcete-li se dozvědět více o řetězení slibů, přečtěte si tuto sekci článku.

Jakýkoli druh schopnosti/složitosti, který slibuje zvládnout s asynchronitou, můžete získat výhody synchronizovaného kódu pomocí generátorů, které yield out slibů (příslibů slibů ...). Je to to nejlepší z obou světů.

runGenerator(..) :Nástroj knihovny

Museli jsme definovat vlastní runGenerator(..) výše uvedený nástroj pro aktivaci a vyhlazení tohoto generátoru + příslib úžasnosti. Dokonce jsme vynechali (kvůli stručnosti) úplnou implementaci takového nástroje, protože je třeba se vypořádat s dalšími nuancemi týkajícími se zpracování chyb.

Nechcete však psát svůj vlastní runGenerator(..) vy?

Nemyslel jsem si to.

Různé příslibové/asynchronní knihovny poskytují právě takový nástroj. Nebudu je zde popisovat, ale můžete se podívat na Q.spawn(..) , co(..) lib atd.

Stručně se však budu věnovat nástroji mé vlastní knihovny:asynquence runner(..) plugin, protože si myslím, že nabízí některé jedinečné schopnosti oproti ostatním. Napsal jsem podrobnou sérii dvoudílných blogových příspěvků na téma asynquence pokud se chcete dozvědět více než jen krátký průzkum zde.

Za prvé, asynkvence poskytuje nástroje pro automatické zpracování zpětných volání typu „chyba jako první“ z výše uvedených úryvků:

function request(url) {
    return ASQ( function(done){
        // pass an error-first style callback
        makeAjaxCall( url, done.errfcb );
    } );
}

To je mnohem hezčí , není to tak!?

Dále asynquence 's runner(..) plugin spotřebovává generátor přímo uprostřed asynkvence sekvence (asynchronní série kroků), takže můžete předávat zprávy z předchozího kroku a váš generátor může předávat zprávy ven do dalšího kroku a všechny chyby se automaticky šíří, jak byste očekávali:

// first call `getSomeValues()` which produces a sequence/promise,
// then chain off that sequence for more async steps
getSomeValues()

// now use a generator to process the retrieved values
.runner( function*(token){
    // token.messages will be prefilled with any messages
    // from the previous step
    var value1 = token.messages[0];
    var value2 = token.messages[1];
    var value3 = token.messages[2];

    // make all 3 Ajax requests in parallel, wait for
    // all of them to finish (in whatever order)
    // Note: `ASQ().all(..)` is like `Promise.all(..)`
    var msgs = yield ASQ().all(
        request( "http://some.url.1?v=" + value1 ),
        request( "http://some.url.2?v=" + value2 ),
        request( "http://some.url.3?v=" + value3 )
    );

    // send this message onto the next step
    yield (msgs[0] + msgs[1] + msgs[2]);
} )

// now, send the final result of previous generator
// off to another request
.seq( function(msg){
    return request( "http://some.url.4?msg=" + msg );
} )

// now we're finally all done!
.val( function(result){
    console.log( result ); // success, all done!
} )

// or, we had some error!
.or( function(err) {
    console.log( "Error: " + err );
} );

Asynkvence runner(..) obslužný program přijímá (volitelné) zprávy ke spuštění generátoru, které pocházejí z předchozího kroku sekvence a jsou přístupné v generátoru v token.messages pole.

Potom, podobně jako jsme předvedli výše s runGenerator(..) nástroj, runner(..) poslouchá buď yield ed slib nebo yield ed asynquence sekvence (v tomto případě ASQ().all(..) sekvence "paralelních" kroků) a čeká na to dokončit před obnovením generátoru.

Když generátor skončí, konečná hodnota je yield s out přejde k dalšímu kroku v sekvenci.

Navíc, pokud se kdekoli v této sekvenci vyskytne jakákoli chyba, dokonce i uvnitř generátoru, zobrazí se na jediném or(..) popisovač chyb registrován.

asynkvence snaží se, aby míchání a párování slibů a generátorů bylo tak smrtelně jednoduché, jak to jen mohlo být. Máte svobodu zapojit jakékoli toky generátoru vedle toků sekvenčních kroků založených na slibech, jak uznáte za vhodné.

ES7 async

Existuje návrh na časovou osu ES7, který vypadá docela pravděpodobně, že bude přijat, vytvořit ještě další druh funkce:async function , což je jako generátor, který je automaticky zabalen do nástroje jako runGenerator(..) (nebo asynquence 's' runner(..) ). Tímto způsobem můžete posílat sliby a async function automaticky je spojí, aby se po dokončení obnovily (není třeba se ani potýkat s iterátory!).

Pravděpodobně to bude vypadat nějak takto:

async function main() {
    var result1 = await request( "http://some.url.1" );
    var data = JSON.parse( result1 );

    var result2 = await request( "http://some.url.2?id=" + data.id );
    var resp = JSON.parse( result2 );
    console.log( "The value you asked for: " + resp.value );
}

main();

Jak můžete vidět, async function lze volat přímo (jako main() ), bez potřeby obslužného programu pro obálkování, jako je runGenerator(..) nebo ASQ().runner(..) zabalit to. Uvnitř, místo použití yield , použijete await (další nové klíčové slovo), které říká async function počkat na dokončení slibu, než budete pokračovat.

V zásadě budeme mít většinu schopností generátorů zabalených v knihovnách, ale přímo podporované nativní syntaxí.

Skvělé, co!?

Mezitím mají knihovny rády asynquence poskytněte nám tyto obslužné programy, aby bylo zatraceně snadné dostat z našich asynchronních generátorů maximum!

Přehled

Jednoduše řečeno:generátor + yield ed slib(y) spojuje to nejlepší z obou světů, aby získal skutečně výkonné a elegantní funkce synchronizace (vypadající) asynchronního řízení toku. S jednoduchými utilitami wrapper (které již poskytuje mnoho knihoven) můžeme automaticky spustit naše generátory až do konce, včetně rozumného a synchronizačního (vypadajícího) zpracování chyb!

A v zemi ES7+ pravděpodobně uvidíme async function to nám umožňuje dělat to i bez nástroje knihovny (alespoň pro základní případy)!

Budoucnost asynchronizace v JavaScriptu je jasná a stále jasnější! Musím nosit stíny.

Tady to ale nekončí. Je tu jeden poslední horizont, který chceme prozkoumat:

Co kdybyste mohli spojit 2 nebo více generátorů dohromady, nechat je běžet nezávisle, ale „paralelně“, a nechat je posílat zprávy tam a zpět, jak postupují? To by byla nějaká super výkonná schopnost, že!?! Tento vzor se nazývá „CSP“ (komunikující sekvenční procesy). V dalším článku prozkoumáme a odemkneme sílu CSP. Dávejte pozor!