Modernizace reaktivity

Reaktivní programování vzalo JavaScript útokem v posledním desetiletí, a to z dobrého důvodu; front-end vývoj velmi těží z jednoduchosti psaní kódu uživatelského rozhraní, které „reaguje“ na změny dat, čímž se eliminuje podstatný imperativní kód náchylný k chybám, jehož úkolem je aktualizovat UI. I když se popularita zvýšila, nástroje a techniky ne vždy držely krok s moderními funkcemi prohlížeče, webovými rozhraními API, jazykovými schopnostmi a optimálními algoritmy pro výkon, škálovatelnost, syntaktickou snadnost a dlouhodobou stabilitu. V tomto příspěvku se podíváme na některé z novějších technologií, technik a schopností, které jsou nyní k dispozici, a předvedeme je v kontextu nové knihovny Alkali.

Mezi techniky, na které se podíváme, patří vykreslování ve frontě, granulární reaktivita založená na tahu, reaktivní generátory a výrazy ES6, reaktivní nativní webové komponenty a reverzibilní směrový tok. Tyto přístupy jsou více než jen programování založené na módě, jsou výsledkem přijatých technologií prohlížečů a postupného výzkumu a vývoje, které poskytují lepší výkon, čistší kód, interoperabilitu s budoucími komponentami a lepší zapouzdření. Opět se podíváme na Alkali pro příklady výsledného jednoduchého stručného deklarativního stylu (můžete skok vpřed viz aplikaci Alkali todo-mvc pro úplnější příklad) se standardní nativní architekturou prvků a možná nejdůležitější funkcí, kterou můžeme vytvořit:rychlý výkon s minimální spotřebou zdrojů. Tyto moderní techniky skutečně přinášejí značný výkon, efektivitu a škálovatelnost. A s neustálým přílivem nových knihoven staví ta nejpreciznější a nejstabilnější architektura přímo na standardech založeném rozhraní API prvků/komponent prohlížeče.

Reaktivita push-pull

Klíčem ke škálování reaktivního programování je architektonický tok dat. Naivním přístupem k reaktivitě je použití jednoduchého pozorovatelného nebo posluchačského vzoru k prosazení každé aktualizace prostřednictvím streamu s každým hodnocením každému posluchači. To může rychle vést k nadměrným výpočtům v jakémkoli typu vícekrokové aktualizace stavu, což vede ke zbytečně opakovaným průběžným hodnocením. Škálovatelnějším přístupem je použití přístupu založeného na "pull", kde jsou jakákoli vypočítaná data vypočítána líně, když následný pozorovatel požaduje nebo "vytáhne" nejnovější hodnotu. Pozorovatelé mohou požadovat data pomocí de-bouncing nebo fronty poté, co byli upozorněni, že se závislá data změnila.

Přístup založený na stahování lze také použít ve spojení s ukládáním do mezipaměti. Jak jsou data vypočítávána, výsledky mohou být ukládány do mezipaměti a oznámení o změnách proti proudu lze použít ke zrušení platnosti mezipaměti po proudu, aby byla zajištěna aktuálnost. Toto schéma mezipaměti a zneplatnění reaktivity založené na tahu sleduje stejnou architekturu návrhu jako REST, škálovatelný design webu, stejně jako architekturu moderních procesů vykreslování prohlížeče.

Existují však situace, kdy je vhodnější mít určité události „tlačeny“, kdy postupně aktualizují aktuální stav. To je užitečné zejména pro progresivní aktualizace kolekce, kde lze položky přidávat, odebírat nebo aktualizovat bez šíření stavu celé kolekce. Nejvýkonnějším přístupem je hybridní:tok dat je primárně získáván od pozorovatele, ale inkrementální aktualizace mohou být prosazovány prostřednictvím živých datových toků jako optimalizace.

Vykreslování ve frontě

Klíčem k využití reaktivních závislostí založených na tahu pro efektivitu v reaktivních aplikacích je zajištění minimalizace provádění vykreslování. Mnoho částí aplikace může často aktualizovat stav aplikace, což může snadno vést k přetěžování a neefektivitě, pokud je vykreslování synchronně prováděno okamžitě při jakékoli změně stavu. Zařazením vykreslování do fronty můžeme zajistit, že i když dojde k více změnám stavu, bude vykreslování minimalizováno.

Zařazení do fronty nebo de-bouncing je poměrně běžná a známá technika. Pro optimální řazení vykreslování do front však prohlížeče ve skutečnosti poskytují vynikající alternativu k obecným funkcím potlačení odrazu. Vzhledem ke svému názvu requestAnimationFrame je často odkázáno na knihovny animací, ale toto moderní API je ve skutečnosti perfektní pro řazení do fronty vykreslování změn stavu. requestAnimationFrame je úkol makro události, takže všechny mikroúkoly, jako jsou usnesení slibů, budou moci být dokončeny jako první. Umožňuje také prohlížečům přesně určit nejlepší načasování pro vykreslení nových změn, přičemž je třeba vzít v úvahu poslední vykreslení, viditelnost karty/prohlížeče, aktuální zatížení atd. Zpětné volání lze provést bez zpoždění (obvykle pod milisekundy) v klidovém viditelném stavu, při vhodné snímkové frekvenci v situacích sekvenčního vykreslování a dokonce zcela odloženo, když je stránka/karta skrytá. Ve skutečnosti řazením do fronty změní stav s requestAnimationFrame a vykreslujeme je podle potřeby pro vizuální aktualizaci, ve skutečnosti sledujeme stejný optimalizovaný tok vykreslování, přesné načasování a sekvenci/cestu, kterou používají samotné moderní prohlížeče. Tento přístup zajišťuje, že pracujeme komplementárním způsobem s prohlížeči, abychom vykreslovali efektivně a včas, aniž bychom museli vynakládat další rozvržení nebo překreslování.

To si lze představit jako dvoufázový způsob vykreslování. První fází je odezva na obsluhu událostí, kde aktualizujeme kanonické zdroje dat, což spustí zneplatnění odvozených dat nebo komponent, které na těchto datech spoléhají. Všechny neplatné komponenty uživatelského rozhraní jsou zařazeny do fronty k vykreslení. Druhá fáze je fáze vykreslování, kdy komponenty získávají svá potřebná data a vykreslují je.

Alkali využívá tuto vykreslenou frontu prostřednictvím svých renderovacích objektů, které propojují reaktivní datové vstupy (v zásadě nazývané "proměnné") s prvkem, a poté řadí všechny změny stavu do fronty pro opětovné vykreslení prostřednictvím requestAnimationFrame mechanismus. To znamená, že jakékoli datové vazby jsou připojeny k vykreslování ve frontě. To lze demonstrovat vytvořením reaktivní hodnoty pomocí Variable konstruktor a jeho připojení k prvku (zde vytvoříme <div> ). Podívejme se na příklad kódu:

import { Variable, Div } from 'alkali'

// create a variable
var greeting = new Variable('Hello')
// create div with the contents connected to the variable
body.appendChild(new Div(greeting)) // note that this is a standard div element
// now updates to the variable will be reflected in the div
greeting.put('Hi')
// this rendering mechanism will be queue the update to the div
greeting.put('Hi again')

Toto připojení automaticky aktualizuje div pomocí requestAnimationFrame mechanismus, kdykoli se stav změní, a více aktualizací nezpůsobí vícenásobné vykreslení, vykreslí se pouze poslední stav.

Granulární reaktivita

Programování čisté funkční reaktivity umožňuje použití jednotlivých signálů nebo proměnných a jejich šíření systémem. V zájmu zachování obeznámenosti s imperativním programováním se však velmi populární reaktivní rámce založené na rozdílech jako ReactJS, které používají virtuální DOM. Ty umožňují psát aplikace stejným způsobem, jakým bychom mohli psát aplikaci s imperativním kódem. Když se stav jakékoli aplikace změní, komponenty se jednoduše znovu vykreslí a po dokončení se výstup komponenty porovná s předchozím výstupem, aby se určily změny. Spíše než explicitní datové toky, které generují konkrétní změny vykresleného uživatelského rozhraní, porovnává diffing výstup opětovného spuštění s předchozími stavy.

I když to může vytvořit velmi známé a pohodlné paradigma pro kódování, stojí to značné náklady, pokud jde o paměť a výkon. Reaktivita rozdílů vyžaduje úplnou kopii vykresleného výstupu a složité algoritmy rozdílů k určení rozdílů a zmírnění nadměrného přepisování DOM. Tento virtuální DOM obvykle vyžaduje 2 až 3krát větší využití paměti než samotný DOM a rozdílové algoritmy zvyšují podobnou režii ve srovnání s přímými změnami DOM.

Na druhé straně skutečné funkční reaktivní programování explicitně definuje „proměnné“ nebo hodnoty, které se mohou měnit, a nepřetržitý výstup těchto hodnot, když se mění. To nevyžaduje žádné další režijní nebo rozdílové algoritmy, protože výstup je přímo specifikován vztahy definovanými v kódu.

Laditelnost také těží z granulárního funkčního reaktivního toku kódu. Ladění imperativního programování zahrnuje opětovné vytváření podmínek a procházení bloků kódu, což vyžaduje komplexní uvažování, aby bylo možné vyhodnotit, jak se stav mění (a jak se kazí). Funkční reaktivní toky lze staticky kontrolovat, kdy máme vždy plnou viditelnost grafu jednotlivých závislých vstupů, které odpovídají výstupu uživatelského rozhraní, a to v jakémkoli okamžiku.

Opět platí, že použití skutečných funkčně reaktivních programovacích technik není pouze esoterickým nebo pedantským úsilím v oblasti počítačové vědy, ale přístupem se smysluplnými a významnými přínosy pro škálovatelnost, rychlost, odezvu, snadné ladění a tok vaší aplikace.

Kanonická a vratná data

Explicitní tok granulární reaktivity také umožňuje obrátit datové toky za účelem dosažení obousměrných vazeb, takže spotřebitelé dat po proudu, jako vstupní prvky, mohou požadovat změny dat proti proudu bez další konfigurace, zapojení nebo imperativní logiky. Díky tomu je extrémně snadné sestavit a svázat vstupní ovládací prvky ve formulářích.

Důležitým principem reaktivity je „jediný zdroj pravdy“, kde existuje explicitní rozdíl mezi kanonickými datovými zdroji a odvozenými daty. Reaktivní data lze popsat jako orientovaný graf dat. To je nezbytné pro koherentní správu dat. Synchronizace více stavů dat bez jasného směru zdrojových a odvozených dat činí správu dat matoucí a vede k různým problémům se správou výpisů.

Jednosměrný tok s centralizovanými změnami dat, spojený s rozdílnou reaktivitou, je jednou z forem správně orientovaného grafu dat. Bohužel jednosměrný tok v konečném důsledku znamená, že spotřebitelé dat musí být ručně připojeni ke zdrojovým datům, což obvykle porušuje princip lokality a postupně degraduje zapouzdření, což má za následek stále více zamotané stavy mezi jinak oddělitelnými a nezávislými komponentami a komplikovanější vývoj formulářů. .

Orientovaný graf s kanonickým zdrojem však nemusí nutně diktovat data, která mohou být prostřednictvím grafu sdělována pouze jedním způsobem. S granulární reaktivitou můžeme podporovat reverzibilní tok. S reverzibilitou lze směrovost stále zachovat definováním změn dat ve směru toku jako upozornění na změnu, která již nastala nebo byla zahájena (v minulosti), zatímco naproti tomu změna dat směrem nahoru je definována jako požadavek na změnu, která má být provedena. iniciované (v budoucnu a odvolatelné). Požadavek na změnu odvozených dat lze stále provádět, pokud má zpětnou transformaci pro šíření požadavku na zdroj (reverzibilní procházení dat nebo transformace se ve funkční terminologii často nazývá „čočka“). Ke změně kanonických dat stále dochází ve zdroji dat, i když ji inicioval/vyžádal následný spotřebitel. S tímto jasným rozlišením toku je stále zachován směrovaný graf kanonických zdrojů a odvozených dat, přičemž je zachována konzistence stavu, přičemž je stále umožněno zapouzdření v interakci s jednotlivými datovými entitami, bez ohledu na to, zda jsou nebo nejsou odvozeny. V praxi to zjednodušuje vývoj uživatelských vstupů a správu formulářů a podporuje zapouzdření vstupních komponent.

Moderní rozšíření DOM („Webové komponenty“)

Předvídavost je kritická pro dlouhodobý vývoj a udržovatelnost, a to je náročné v ekosystému JavaScriptu, kde se neustále objevují četné technologie. Jaký nový rámec bude vzrušující za tři roky? Pokud je minulost nějakým ukazatelem, je velmi těžké to předvídat. Jak se vyvíjíme s tímto typem churn? Nejspolehlivějším přístupem je minimalizovat naši závislost na API specifických pro knihovny a maximalizovat naši závislost na standardních API a architektuře prohlížečů. A s nově vznikajícími komponentami API a funkcemi (známými jako „webové komponenty“) se to stává mnohem proveditelnějším.

Dobře definované reaktivní struktury by neměly určovat konkrétní architekturu komponent a flexibilita použití nativních komponent nebo komponent třetích stran maximalizuje možnosti budoucího vývoje. I když však můžeme a měli bychom minimalizovat propojení, určitá úroveň integrace může být užitečná. Zejména možnost přímo používat proměnné jako vstupy nebo vlastnosti je jistě pohodlnější, než muset vytvářet vazby až poté. A integrace s životním cyklem prvku/komponenty a upozornění, kdy jsou prvky odstraněny nebo odpojeny, může usnadnit automatické čištění závislostí a naslouchacích mechanismů, aby se zabránilo únikům paměti, minimalizovala spotřeba zdrojů a zjednodušilo používání komponent.

Moderní prohlížeče opět učinily tento typ integrace s nativními prvky zcela proveditelným. Nyní je možné rozšířit stávající prototypy HTML o skutečné vlastní třídy založené na DOM s reaktivními konstruktory s proměnnými a MutationObserver rozhraní (a potenciální budoucí zpětná volání webových komponent) nám dávají možnost sledovat, kdy jsou prvky odpojeny (a připojeny). Funkce getter/setter představená v ES5 nám také umožňuje správně rozšířit a reprodukovat vlastnosti stylu nativních prvků.

Alkali definuje sadu konstruktorů/tříd DOM s přesně touto funkčností. Tyto třídy jsou minimálními rozšířeními nativních tříd DOM s konstruktory s argumenty, které podporují proměnné vstupy, které řídí vlastnosti, a automatické čištění proměnných. Ve spojení s línou/pull-based reaktivitou to znamená, že prvky reaktivně zobrazují data, zatímco jsou viditelné, a jakmile jsou odpojeny, již nebudou spouštět žádná hodnocení díky své závislosti na vstupech. Výsledkem je vytvoření a rozšíření prvku s automatickým samočištěním posluchačů. Například:

let greetingDiv = new Div(greeting)
body.appendChild(greetingDiv)
// a binding will be created that listens for changes to greeting
...
body.removeChild(greetingDiv)
// binding/listener of greeting will be cleaned up

Reaktivní generátory

Nejen webová rozhraní API poskytují důležitá vylepšení v našem přístupu k reaktivitě, ale samotný jazyk ECMAScript má vzrušující nové funkce, které lze použít ke zlepšení syntaxe a snadného psaní reaktivního kódu. Jednou z nejvýkonnějších nových funkcí jsou generátory, které poskytují elegantní a intuitivní syntaxi pro interaktivní tok kódu. Asi největší nepříjemností práce s reaktivními daty v JavaScriptu je častá potřeba funkcí zpětného volání pro zpracování změn stavu. Nové funkce generátoru ECMAScript však poskytují možnost pozastavit, obnovit a restartovat funkci tak, že funkce může využívat reaktivní datové vstupy se standardní sekvenční syntaxí, pozastavení a obnovení pro jakékoli asynchronní vstupy. Regulátory generátoru se mohou také automaticky přihlásit k závislým vstupům a znovu spustit funkci, když se vstupy změní. Toto řízení vykonávání funkcí, které umožňují generátory, lze využít k získání (zamýšlené slovní hříčkou!) intuitivní a snadno pochopitelné syntaxe pro složité kombinace proměnných vstupů.

U generátorů se očekávalo, jak eliminují zpětná volání se sliby a umožňují intuitivní sekvenční syntaxi. Ale generátory mohou jít ještě dále, aby se nejen pozastavily a obnovily pro asynchronní vstup, ale restartovaly se, když se jakákoliv vstupní hodnota změní. Toho lze dosáhnout pomocí yield operátor před libovolným vstupem proměnné, který umožňuje koordinačnímu kódu naslouchat změnám proměnné a vrátit aktuální hodnotu proměnné do yield výraz, když je k dispozici.

Pojďme se podívat na to, jak je toho dosaženo. V Alkali lze funkce generátoru použít jako transformaci vstupních proměnných k vytvoření reaktivní funkce, která vydává novou složenou proměnnou s react . react Funkce funguje jako regulátor generátoru pro zpracování reaktivních proměnných. Pojďme si to rozebrat na příkladu:

let a = new Variable(2)
let aTimesTwo = react(function*() {
  return 2 * yield a
})

react řadič zpracovává spouštění poskytnutého generátoru. Funkce generátoru vrací iterátor, který se používá k interakci s generátorem, a react spustí iterátor. Generátor bude pracovat, dokud nevyhodnotí yield operátor. Zde kód okamžitě narazí na yield a vraťte řízení na react funkce s hodnotou poskytnutou yield operátor se vrátil z iterátoru. V tomto případě a proměnná bude vrácena na react funkce. To dává react funkce příležitost dělat několik věcí.

Za prvé, může se přihlásit nebo poslouchat poskytnutou reaktivní proměnnou (pokud je jedna), takže může reagovat na jakékoli změny opětovným spuštěním. Za druhé, může získat aktuální stav nebo hodnotu reaktivní proměnné, takže ji může vrátit zpět jako výsledek yield výraz, při obnovení. Nakonec před vrácením ovládacího prvku react Funkce může zkontrolovat, zda je reaktivní proměnná asynchronní, drží příslib na hodnotě a čeká na vyřešení příslibu před obnovením provádění, pokud je to nutné. Jakmile je načten aktuální stav, může být funkce generátoru obnovena s hodnotou 2 vráceno z yield a výraz. Pokud je více yield výrazy, které se objeví, budou postupně vyřešeny stejným způsobem. V tomto případě pak generátor vrátí hodnotu 4 , což ukončí sekvenci generátoru (do a změny a je znovu spuštěn).

S alkalickým react Tato funkce je zapouzdřena v jiné složené reaktivní proměnné a jakékoli změny proměnných nespustí opětovné spuštění, dokud k nim nepřistupují data nebo si je nevyžádají.

Funkce alkalického generátoru lze také použít přímo v konstruktorech prvků k definování vykreslovací funkce, která se automaticky znovu spustí, kdykoli se změní vstupní hodnota. V obou případech pak použijeme yield před jakoukoli proměnnou. Například:

import { Div, Variable } from 'alkali'
let a = new Variable(2)
let b = new Variable(4)
new Div({
  *render() {
    this.textContent = Math.max(yield a, yield b)
  }
})

Tím se vytvoří <div> s textovým obsahem 4 (maximální ze dvou vstupních hodnot). Můžeme aktualizovat kteroukoli proměnnou a ona se znovu spustí:

a.put(5)

<div> by nyní byl aktualizován tak, aby měl obsah 5 .

Generátory nejsou univerzálně dostupné ve všech prohlížečích (ne v IE a Safari), ale generátory lze transpilovat a emulovat (pomocí Babel nebo jiných nástrojů).

Vlastnosti a proxy

Reaktivní vazba na vlastnosti objektu je důležitým aspektem reaktivity. Ale zapouzdřit vlastnost s oznámením změn vyžaduje více než jen aktuální hodnotu vlastnosti vrácenou standardním přístupem k vlastnosti. V důsledku toho mohou vazby reaktivních vlastností nebo proměnné vyžadovat podrobnou syntaxi.

Další vzrušující novinkou v ECMAScriptu jsou však proxy, které nám umožňují definovat objekt, který dokáže zachytit veškerý přístup k vlastnostem a úpravy pomocí vlastních funkcí. Toto je výkonná funkce, kterou lze použít k vrácení proměnných reaktivních vlastností prostřednictvím běžného přístupu k vlastnostem, což umožňuje pohodlnou idiomatickou syntaxi s reaktivními objekty.

Bohužel proxy nelze tak snadno emulovat pomocí kompilátorů kódu, jako je Babel. Emulování proxy serverů by vyžadovalo nejen transpilaci samotného konstruktoru proxy, ale jakéhokoli kódu, který by mohl k proxy přistupovat, takže emulace bez podpory nativního jazyka by byla buď neúplná, nebo by byla nepřiměřeně pomalá a nabubřelá kvůli masivní transpilaci požadované pro každý přístup k vlastnosti v aplikaci. Je však možná cílenější transpilace reaktivního kódu. Podívejme se na to.

Reaktivní výrazy

Zatímco EcmaScript se neustále vyvíjí, nástroje jako Babel a jeho funkce zásuvných modulů nám dávají obrovské příležitosti k vytváření nových kompilovaných jazykových funkcí. A zatímco generátory jsou úžasné pro vytváření funkcí se sérií kroků, které se mohou provádět asynchronně a reaktivně, pomocí pluginu Babel lze kód transformovat tak, aby skutečně vytvářel plně reaktivní datové toky, s vazbami vlastností, pomocí syntaxe ECMAScript. To jde dál než k pouhému opětovnému spuštění, ale výstup výrazů lze definovat ve vztahu ke vstupům tak, že lze pomocí jednoduchých idiomatických výrazů generovat reverzibilní operátory, reaktivní vlastnosti a reaktivní přiřazení.

Samostatný projekt obsahuje alkalický babel plugin pro transformaci reaktivních výrazů. S tímto můžeme napsat normální výraz jako argument do react hovor/operátor:

let aTimes2 = react(a * 2)

Toto aTimes2 bude vázán na násobení vstupní proměnné. Pokud změníme hodnotu a (pomocí a.put() ), aTimes2 se automaticky aktualizuje. Ale protože se ve skutečnosti jedná o obousměrnou vazbu přes dobře definovaný operátor, data jsou také vratná. aTimes2 můžeme přiřadit novou hodnotu z 10 a poté a bude aktualizováno na hodnotu 5 .

Jak již bylo zmíněno, je téměř nemožné emulovat proxy přes celou kódovou základnu, ale v rámci našich reaktivních výrazů je velmi rozumné zkompilovat syntaxi vlastností, aby bylo možné vlastnosti zpracovávat jako reaktivní proměnné. Kromě toho mohou být další operátory transpilovány na vratné transformace proměnných. Například bychom mohli psát složité kombinace s plně reaktivním kódem na jazykové úrovni:

let obj, foo
react(
  obj = {foo: 10}, // we can create new reactive objects
  foo = obj.foo, // get a reactive property
  aTimes2 = foo // assign it to aTimes2 (binding to the expression above)
  obj.foo = 20 // update the object (will reactively propagate through foo, aTimes2, and to a)
)
a.valueOf() // -> 10

Modernizace

Vývoj webu je vzrušující svět neustálých změn a pokroku. A reaktivita je výkonný programovací koncept pro zvukovou architekturu pokročilých aplikací. Reaktivita může a měla by růst a využívat nejnovější nové technologie a schopnosti moderního prohlížeče a jeho jazyka a API. Společně mohou přinést další krok vpřed ve vývoji webu. Jsem nadšený z těchto možností a doufám, že tyto nápady mohou posunout způsoby, jak můžeme využít budoucnost pomocí nových nástrojů.

Alkali byl vyvinut v době, kdy náš inženýrský tým ve společnosti Doctor Evidence pracoval na vytváření interaktivních a citlivých nástrojů pro zkoumání, dotazování a analýzu velkých souborů dat z klinických lékařských studií. Udržet hladké a interaktivní uživatelské rozhraní se složitými a rozsáhlými daty byla fascinující výzva a mnoho z těchto přístupů pro nás bylo velmi užitečných, protože při vývoji našeho webového softwaru přijímáme novější technologie prohlížečů. Pokud nic jiného, ​​doufejme, že Alkali může sloužit jako příklad, který inspiruje další kroky vpřed ve vývoji webu.