asynquence:More than Just Promises (část 2)

Toto je vícedílná série blogových příspěvků zdůrazňující schopnosti asynkvence, nástroje pro abstrakci řízení toku založeného na slibech.

  • Část 1:Sliby, které ještě neznáte
  • Část 2:Více než jen sliby

asynquence Is Promises

Jak jsme viděli v části 1, asynquence je obalová abstrakce nad sliby, jako sekvence . Jednokroková sekvence se blíží slibu, i když nejsou identicky kompatibilní. To však není velký problém, protože asynquence může snadno konzumovat i prodávat standardní přísliby/theables.

Takže, o co jde? "Nepotřebuji abstrakce slibů, protože mě jejich omezení netrápí." Nebo:„Už mám příslibovou abstrakci/rozšíření lib, která se mi líbí, je opravdu populární!“

V jistém smyslu s takovým sentimentem souhlasím. Pokud ještě nevidíte potřebu pro asynkvenci , nebo pokud vás jeho chuť neláká, chápu, že se necítím nucen na něj přejít.

Ale to jsme teprve poškrábali povrch asynquence . Pokud se jen zastavíte zde, unikli vám mnohem větší obrázek. Prosím, čtěte dál.

asynquence Je mnohem více... a roste!

Nejprve bychom měli mluvit o asynkvenci lze rozšířit tak, aby dělal více, než se dodává. Myslím si, že toto je jedna z nejzajímavějších částí nástroje, zejména vzhledem k tomu, jak malý je balíček a jak málo jeho kolegů (dokonce mnohem větších) poskytuje tuto úroveň schopností.

Celý seznam asynquence-contrib pluginy jsou poskytovány jako volitelná rozšíření jádra asynquence schopnost. To znamená, že jsou skvělým místem, kde můžete začít zkoumat, jak byste mohli vytvářet svá vlastní rozšíření.

Několik z nich pouze přidá další statické pomocníky do ASQ jmenný prostor, například ASQ.iterable(..) (ke kterému se dostaneme později). Většina z nich však do rozhraní API instance přidává řetězitelné metody, takže můžete dělat věci, jako je volání first(..) plugin v řetězci uprostřed sekvence, například ASQ().then(..).first(..).then(..).. . To je docela silné.

Představme si jednoduchý scénář:Zjistíte, že pravidelně chcete zaznamenávat (například do vývojářské konzole) hodnotu nějaké zprávy, když prochází určitým krokem vaší sekvence. Běžně to děláte takto:

ASQ(..)
.then(..)
.val(function(msg){
    console.log(msg);
    return msg;
})
.then(..)
..

Bylo by hezké mít znovu použitelný způsob, jak to udělat? Můžete deklarovat jeden, například:

function ASQlog(msg) {
    console.log(msg);
    return msg;
}

ASQ(..)
.then(..)
.val( ASQlog )
.then(..)
..

Můžeme to však ještě vylepšit pomocí našeho vlastního pluginu pro příspěvky. Za prvé, zde je návod, jak jej používáme:

ASQ(..)
.then(..)
.log()
.then(..)
..

Ooo, to je hezčí! jak to uděláme? V kořenovém adresáři balíčku contrib vytvořte soubor s názvem „plugin.log.js“ a vložte do něj něco takového:

ASQ.extend( "log", function __log__(api,internals){
    return function __log__() {
        api.val(function(msg){
            console.log(msg);
            return msg;
        });

        return api;
    };
});

To je snadné, že!? V podstatě bez ohledu na běžné použití veřejného ASQ API, které často opakujete, můžete zabalit stejný druh volání

Udělejme to teď trochu robustnější (aby bylo možné zpracovat více než jednu zprávu o úspěchu) a také nechte jej odhlásit všechny chyby:

ASQ.extend( "log", function __log__(api,internals){
    return function __log__() {
        api.val(function(){
            console.log.apply(console,arguments);
            return ASQ.messages.apply(null,arguments);
        })
        .or(function(){
            console.error.apply(console,arguments);
        });

        return api;
    };
});

Zde vidíte použití ASQ.messages(..) utility. To je jednoduchý způsob, jak vytvořit pole hodnot, které je specificky označeno značkou ASQ aby bylo možné pole rozpoznat a rozbalit (do pozičních parametrů), kde je to vhodné.

Udělejme další hloupý příklad:

ASQ("foo and bar are awesome!")
.fOObAR()
.log(); // "fOO and bAR are awesome!"

Jak?

ASQ.extend( "fOObAR", function __fOObAR__(api,internals){
    return function __fOObAR__() {
        api.val(function(msg){
            return msg
                .replace(/\bfoo\b/g,"fOO")
                .replace(/\bbar\b/g,"bAR");
        });

        return api;
    };
});

Opakovatelné sekvence

Když se podíváte na to, jak sekvence fungují, vnitřně se posouvaly zavoláním příslušného spouštěče každého kroku (stejně jako sliby). Ale určitě existují případy, kdy by bylo hezké posouvat sekvenci zvenčí.

Představme si například jednorázovou událost jako DOMContentLoaded , kde potřebujete pokročit v hlavní sekvenci pouze tehdy, když tato událost nastane.

Zde je návod, jak jej musíte "hacknout", pokud máte pouze asynkvenci jádro:

ASQ(function(done){
    document.addEventListener("DOMContentLoaded",done,false);
})
.then(..)
..

Nebo uděláte „extrakce schopností“ (bohužel častější v Promises, než si myslím, že by mělo být), abyste lépe oddělili obavy/schopnosti:

var trigger;

ASQ(function(done){
    trigger = done; // extract the trigger
})
.then(..)
..

// later, elsewhere
document.addEventListener("DOMContentLoaded",trigger,false);

Všechny tyto možnosti a jejich variace jsou k ničemu, zvláště když vezmete v úvahu vícekrokovou inicializaci před spuštěním hlavní sekvence, jako je DOMContentLoaded spouštění a vrací se požadavek na počáteční nastavení Ajaxu.

Nyní tedy představujeme poněkud odlišný koncept, který poskytuje iterable(..) plugin:iterable-sequences . Jedná se o sekvence, které nejsou interně pokročilé, ale jsou namísto toho pokročilé externě pomocí známého iterátoru rozhraní:.next(..) .

Každý krok iterovatelné sekvence nemá svůj vlastní spouštěč a také neexistují žádné automaticky předávané zprávy o úspěchu z kroku na krok. Místo toho předáte zprávu s next(..) a na konci kroku získáte hodnotu zpět (operace, která je sama o sobě zásadně synchronní). "Asynchronní" povaha těchto sekvencí je externí vzhledem k sekvenci, skrytá v jakékoli logice, která řídí iteraci sekvence.

DOMContentLoaded příklad:

var trigger = ASQ.iterable();

document.addEventListener("DOMContentLoaded",trigger.next,false);

// setup main async flow-control
ASQ( trigger ) // wait for trigger to fire before proceeding
.then(..)
.then(..)
..

Nebo pro více kroků:

var noop = function(){};
var setup = ASQ.iterable().then(noop);

document.addEventListener("DOMContentLoaded",setup.next,false);
ajax("some-url",function(response){
    // do stuff with response
    setup.next();
});

// setup main async flow-control
ASQ( setup ) // wait for setup to complete before proceeding
.then(..)
.then(..)
..

Opakování iterovatelných sekvencí

Iterovatelné sekvence lze také nastavit tak, aby měly předdefinovanou (nebo dokonce nekonečnou) sadu kroků, a poté je lze iterovat pomocí normálních iteračních technik.

Chcete-li například ručně synchronizovat iterovatelnou sekvenci s for smyčka:

function double(x) { return x * 2; }
function triple(x) { return x * 3; }

var isq = ASQ.iterable()
.then(double)
.then(double)
.then(triple);

for (var seed = 3, ret;
    (ret = isq.next(seed)) && !ret.done;
) {
    seed = ret.value;
    console.log(seed);
}
// 6
// 12
// 36

Ještě lepší je, že ES6 nám dává @@Iterator háčky plus for..of smyčka, aby se automaticky opakovaly iterovatelné sekvence (za předpokladu, že každý krok nepotřebuje vstup):

var x = 0;
function inc() { return ++x; }

var isq = ASQ.iterable()
.then(inc)
.then(inc)
.then(inc);

for (var v of isq) {
    console.log(v);
}
// 1
// 2
// 3

Samozřejmě, toto jsou příklady synchronního iterování iterovatelné sekvence, ale je triviální si představit, jak voláte next(..) uvnitř asynchronních úloh, jako jsou časovače, obslužné rutiny událostí atd., což má za následek asynchronní procházení kroků iterovatelné sekvence.

Tímto způsobem jsou iterovatelné sekvence něco jako generátory (kterým se budeme věnovat dále), kde každý krok je jako yield a next(..) restartuje sekvenci/generátor.

Generátory

Kromě Promise ES6 přidává funkci generátorů, což je další obrovský doplněk ke schopnosti JS zvládat asynchronní programování rozumněji.

Nebudu zde učit všechny generátory (je o nich již napsáno mnoho). Dovolte mi však pro ilustraci rychle nakódovat předchozí příklad pomocí generátoru:

function* gen() {
    var x = 0;
    yield ++x;
    yield ++x;
    yield ++x;
}
for ( var v of gen() ) {
    console.log(v);
}
// 1
// 2
// 3

Jak můžete vidět, generátory v podstatě vypadají jako synchronní kód, ale yield klíčové slovo jej pozastaví uprostřed provádění a volitelně vrátí hodnotu. for..of smyčka skryje next() volá, a tudíž nic neposílá, ale můžete ručně iterovat generátor, pokud byste potřebovali předávat hodnoty při každé iteraci, stejně jako jsem to udělal výše s iterovatelnými sekvencemi.

Ale to není nejlepší část generátorů. Skvělá část je, když jsou generátory kombinovány se sliby. Například:

function asyncIncrement(x) {
    return new Promise(function(resolve){
        setTimeout(function(){
            resolve(++x);
        },500);
    });
}

runAsyncGenerator(function*(){
    var x = 0;
    while (x < 3) {
        x = yield asyncIncrement(x);
    }
    console.log(x);
});
// 3

Některé velmi důležité věci, kterých si musíte všimnout:

  1. Použil jsem nějaký mýtický runAsyncGenerator(..) utility. K tomu se za chvíli vrátíme.
  2. Co jsme yield z našeho generátoru je ve skutečnosti příslib hodnoty, spíše než okamžitá hodnota. Po dokončení našeho slibu samozřejmě něco dostaneme zpět a to něco je navýšené číslo.

Uvnitř runAsyncGenerator(..) obslužný program, měl bych iterátor ovládající můj generátor, který by volal next(..) na něm postupně.

Co získá zpět z next(..) hovor je příslib , takže jen posloucháme, až tento slib skončí, a když se tak stane, vezmeme jeho hodnotu úspěchu a předáme ji zpět do dalšího next(..) zavolejte.

Jinými slovy, runAsyncGenerator(..) automaticky a asynchronně spustí náš generátor až do konce, přičemž každý „krok“ asynchronního slibu pouze pozastaví iteraci až do vyřešení.

Jedná se o nesmírně výkonnou techniku, protože nám umožňuje psát synchronizovaný kód, jako je náš while smyčka, ale schovat se jako detail implementace skutečnost, že sliby jsme yield out zavést asynchronicity do iterační smyčky.

asynquence ?

Několik dalších async/promises knihoven má nástroj jako runAsyncGenerator(..) již vestavěný (nazývaný spawn(..) nebo co(..) , atd). A stejně tak asynquence s názvem runner(..) . Ale ta jedna asynkvence poskytuje je mnohem výkonnější!

Nejdůležitější je ta asynkvence umožňuje zapojit generátor, aby běžel přímo uprostřed normální sekvence, jako je specializovaný then(..) druh kroku, který také umožňuje předat zprávy o kroku předchozí sekvence do generátor a umožňuje vám yield hodnota(y) z konce generátoru pokračovat v hlavní sekvenci.

Pokud je mi známo, žádná jiná knihovna takovou schopnost nemá! Podívejme se, jak to vypadá:

function inc(x,y) {
    return ASQ(function(done){
        setTimeout(function(){
            done(x + y);
        },500);
    });
}

ASQ( 3, 4 )
.runner(function*(control){
    var x = control.messages[0];
    var y = control.messages[1];

    while (x < 20) {
        x = yield inc(x,y);
    }

    // Note: `23` was the last value yielded out,
    // so it's automatically the success value from
    // the generator. If you wanted to send some
    // other value out, just call another `yield __`
    // here.
})
.val(function(msg){
    console.log(msg); // 23
});

inc(..) zobrazený vrací asynkvenci instanci, ale fungoval by stejně, kdyby vrátil normální příslib, jako runner(..) naslouchá slibům nebo sekvencím a náležitě s nimi zachází. Samozřejmě, pokud byste chtěli, mohli jste získat mnohem složitější, vícekrokovou sekvenci (nebo řetězec slibů) a runner(..) jen by seděl a trpělivě čekal.

To je docela síla, nemyslíte!? Generators + Promises nepochybně představují budoucí směr asynchronního programování v JS. Ve skutečnosti první návrhy pro ES7 naznačují, že dostaneme async funkce, které budou mít nativní syntaktickou podporu pro to, co spawn(..) a runner(..) dělat. Super vzrušující!

Ale to je jen stěží poškrábání povrchu toho, jak asynquence využívá sílu generátorů.

Souběh ve stylu CSP (jako go)

Právě jsme viděli sílu jediného generátoru, který je spuštěn do dokončení uprostřed sekvence.

Ale co kdybyste spárovali dva nebo více generátorů dohromady tak, že se vzájemně podvolují tam a zpět? V podstatě byste dosáhli stylu CSP (C komunikující S sekvenční P rocesses) souběžnost, kde každý generátor byl jako sekvenční „proces“ a kooperativně prokládaly své vlastní jednotlivé kroky. Mají také sdílený kanál pro zasílání zpráv mezi nimi.

Nemohu přeceňovat sílu tohoto vzoru.

Je to v podstatě to, co jede jazyk přirozeně podporuje a to, co ClojureScript's core.async funkce se automaticky vytvoří v JS. Velmi doporučuji, abyste si přečetli fantastické spisy Davida Nolena na toto téma, jako je tento příspěvek a tento příspěvek, stejně jako další. Podívejte se také na jeho rámec Om, který využívá tyto nápady a další.

Ve skutečnosti existuje také samostatná knihovna přesně pro tuto úlohu souběžnosti ve stylu CSP, nazvaná js-csp.

asynquence ve stylu CSP

Ale tento příspěvek je o asynkvenci , že jo? Spíše než potřeba samostatné knihovny nebo jiného jazyka, síla asynquence spočívá v tom, že můžete programovat ve stylu CSP pomocí stejného nástroje, s jakým fungují všechny ostatní sliby.

Spíše než úplné vyučování celého konceptu se rozhodnu jej pouze ilustrovat pomocí kódu a nechám vás zkoumat a učit se, do jaké míry vás to zaujme. Osobně se domnívám, že toto je velká část budoucnosti pokročilého asynchronního programování v tomto jazyce.

Chystám se rip/fork/port tento příklad přímo z go a js-csp ... klasický ukázkový příklad "Ping Pong". Chcete-li vidět, jak to funguje, spusťte ukázku v prohlížeči (Poznámka: v současné době pouze generátory Chrome vyhovují specifikacím dostatečně pro spuštění příkladu – FF je blízko, ale ne úplně tam).

Fragment kódu ukázky:

ASQ(
    ["ping","pong"], // player names
    { hits: 0 } // the ball
)
.runner(
    referee,
    player,
    player
)
.val(function(msg){
    console.log("referee",msg); // "Time's up!"
});

Stručně řečeno, pokud si prozkoumáte úplný kód JS na tomto odkazu na ukázku, můžete vidět 3 generátory (referee a dvě instance player ), které provozuje runner(..) , řízení obchodování mezi sebou (podle yield table výpisy) a vzájemné zasílání zpráv prostřednictvím sdílených kanálů zpráv v table.messages .

Stále můžete získat přísliby/sekvence z generátoru, jako yield sleep(500) ano, což nepřenáší řízení, ale pouze pozastavuje postup generátoru, dokud se slib/sekvence nedokončí.

Opět... wow. Generátory spárované dohromady jako korutiny ve stylu CSP je obrovský a do značné míry nevyužitý horizont, ke kterému se právě začínáme přibližovat. asynkvence je na špici tohoto vývoje a umožňuje vám prozkoumat sílu těchto technik vedle známějších možností slibů. Žádné přepínání rámců – vše je v jednom nástroji.

Reaktivní na události

OK, poslední pokročilý vzorec, který zde prozkoumám s asynkvencí je vzor "reactive pozorovatelných" z knihovny RxJS -- Reactive Extensions od chytrých lidí (jako Matt Podwysocki) z Microsoftu. Nechal jsem se inspirovat jejich „reaktivními pozorovateli“ a přidal jsem podobný koncept, kterému říkám „reaktivní sekvence“, prostřednictvím react(..) plugin.

Stručně řečeno, problém, který chceme řešit, je ten, že sliby fungují dobře pouze pro události typu single-fire. Co kdybyste měli opakující se událost (např. kliknutí na tlačítko), kterou chcete spustit sled událostí pro každé pravidlo?

Mohli bychom to udělat takto:

$("#button").click(function(evt){
    ASQ(..)
    .then(..)
    .then(..)
    ..
});

Ale to trochu naštve oddělení zájmů/schopností. Rádi bychom byli schopni oddělit specifikaci sekvence řízení toku od naslouchání události, která ji spustí. Jinými slovy, rádi bychom invertovali „vnoření“ tohoto příkladu.

Asynkvence react(..) plugin vám tuto možnost poskytuje:

var sq = ASQ.react(function(trigger){
    $("#button").click(trigger);
});

// elsewhere:
sq
.then(..)
.then(..)
..

Pokaždé trigger je volána funkce, nová kopie z definované sekvence (aka šablony) se oddělí a běží nezávisle.

Ačkoli to zde není zobrazeno, můžete také zaregistrovat kroky, které je třeba podniknout při bourání reaktivní sekvence (k uvolnění ovladačů atd.). K dispozici je také speciální pomocník pro poslech událostí ve streamech node.js.

Zde je několik konkrétních příkladů:

  1. DEMO:Reaktivní sekvence + gate(..)
  2. KÓD:Reaktivní sekvence + node.js HTTP streamy

Sečteno a podtrženo, můžete snadno přejít na používání celé knihovny RxJS (je poměrně velká/složitá, ale extrémně schopná!) pro takové asynchronní programování reagující na události nebo můžete použít *asynquence a získejte některé z těchto důležitých funkcí přímo zabudovaných do obslužného programu, který již zpracovává vaše další úkoly asynchronního řízení toku.

Zabalení

Myslím, že nyní můžete pravděpodobně souhlasit:je to celá řada pokročilých funkcí a vzorů, které získáte ihned po vybalení pomocí asynkvence .

Doporučuji vám dát asynqueni a uvidíme, jestli to nezjednoduší a nezmění vaše asynchronní kódování v JS.

A pokud najdete něco, co z hlediska funkčnosti podstatně chybí, vsadím se, že můžeme napsat plugin, který to udělá docela snadno!

Zde je to nejdůležitější, co vám mohu nechat:Nenapsal jsem asynquence nebo tuto sérii blogových příspěvků jen takže byste použili lib (i když doufám, že to zkusíte). Vytvořil jsem to otevřeně a napsal jsem tyto veřejné příspěvky, abych vás inspiroval, abyste mi pomohli udělat to lepší a lepší.

Chci asynkvenci být nejvýkonnější sbírkou asynchronních nástrojů pro řízení toku kdekoli. Můžete mi pomoci aby se to stalo.