Întotdeauna am fost curios să înțeleg cum funcționează lucrurile. Ca programator, atunci când folosești librării și frameworkuri noi, poate deveni destul de confuz tot procesul, pentru că nu știi exact cum funcționează lucrurile "în culise". Astfel că frameworkuri precum Django și Angular mi-au dat întotdeauna impresia că ceva "magic" se întâmplă fără ca eu să știu. În ultimii ani, am început să fiu curios referitor la cum funcționează mai exact lucrurile în aceste frameworkuri, pe care le folosim zi de zi. Spre exemplu, ce se întâmplă cu template-urile interpolate sau JSX și cum sunt convertite în HTML? Cum funcționează variabilele reactive, și ce înseamnă mai exact Virtual DOM?
Prin urmare, am ajuns să mă interesez despre modul în care reactivitatea a devenit o componentă necesară în ecosistemul JavaScript, despre modul în care a evoluat și despre direcția în care se îndreaptă momentan.
Dacă ne uităm puțin la trecutul limbajului și la modul în care a evoluat acesta, putem să observăm niște tipare care au existat din 1995 și până azi, care au influențat apariția conceptului Signals.
La începutul anilor 2000, totul era server-side. Limbajele compilate sau dinamice erau în vogă, alături de frameworkurile lor specializate pentru web. PHP și Symphony, ASP.NET în C#, Servlet și Spring pentru Java, Rails în Ruby sau Django pentru Python. Iar, în acele vremuri, JavaScript nu era privit la fel cum este privit astăzi. Era considerat mai mult un 'limbaj de jucărie", folosit pentru widgeturi mici și izolate, sau interacțiuni customizate după ce pagina era servită de către server. Nu existau module, nu exista NPM, iar toate variabilele erau globale.
Apoi, am avut parte de concepte precum XHR, Ajax și Google Web Toolkit care au permis librăriilor precum Dojo, Mootools și JQuery să prindă viață și să ajute programatorii prin interacțiuni simplificate ale DOM-ului și extensii aduse limbajului. În 2007 avem parte de apariția smarthphone-urilor, și astfel apare și nevoie de separare dintre client și server.
Marea revoluție a librăriilor vine cu nume mari, precum Knockout, Backbone și Angular.js, care apar cam în aceeași perioadă, fiind urmate la câțiva ani de React și Vue. Le numim librării, pentru că încercau să facă un singur lucru și să îl facă bine, respectiv să rezolve problemele de rendering pe care programatorii JavaScript le aveau în acea perioadă. Astfel, le putem privi ca și niște view-layer libraries, ușor de implementat într-un proiect deja existent pe care puteai apoi să îl convertești incremental pentru a folosi aceste noi tehnologii.
Urmează revoluția frameworkurilor care aduc suport pe mai multe paliere decât cel de rendering, oferind full-stack support alături de routing, server-side rendering, data management, modularitate, extensibilitate și management de dependințe etc. Amintim câteva: Angular, Next, Nuxt, SvelteKit, Remix, Astro, Quick, iar lista poate să continue.
Apoi, în 2020, SolidJS vine cu un conceptul de Signals. Și încet dar sigur, toată lumea începe să îl adopte și să îl promoveze ca noul mod de facto de a face state management. Vue îl va numi ref, Angular, Preact și Qwik îl implementează ca signal, iar Svelte îi va folosi magia prin intermediul $runes. Practic, toată lumea își dă seama că Knockout a avut dreptate acum 15 ani când a implementat același tip de reactivitate fine-grained prin observable și computed.
Pe scurt, un Signal este o primitivă reactivă, folosită pentru managementul state-ului unei aplicații. Ca inspirație, pornește de la un Design Pattern, respectiv cel de Observer, prin care oferă un API simplificat bazat pe un set de principii (features) de reactivitate. Astfel permite o experiență de development ergonomică, alături de o implementare bine optimizată pentru procesul de rendering.
API-ul este format din trei părți:
Root State - cel care ține valoarea.
Derived State - sau computed state este bazat pe unul sau mai multe root states și se schimbă automat când una din dependințele lui se modifică și ea.
Dependency tracking - abilitatea de a putea urmări dependințele între state-uri. Aceasta trebuie să fie optimizată și automată, rezultând într-un grafic de dependințe care reprezintă automat toate legăturile realizate.
Lazy by default - Dacă un copac cade în pădure, și nimeni nu l-a auzit căzând, face zgomot? Astfel, dacă un state nu este legat în nici un fel de alte state-uri sau efecte, acesta nu face parte din graficul de dependințe, deci nu trebuie urmărit. De asemenea, state-ul derivat este evaluat doar atunci când o dependință este invocată și nu în momentul în care este declarat.
Memorization - Pe scurt, ultima valoare a unui state este cache-uită. Dacă nu și-a schimbat valoarea, o să știm întotdeauna care este valoarea actuală.
Aceste principii stau la baza implementării semnalelor, iar prin folosirea lor rezultă următoarele "core features":
Fine Grained Reactivity - evităm recalculări și randări care nu sunt necesare. Astfel experiența utilizatorului este îmbunătățită și optimizată.
Mai jos să găsim un exemplu rudimentar prin care implementăm în câteva linii de cod conceptul de Signal. Acesta oferă o funcție numită signal pentru crearea de root state care ține o valoare, și o funcție de effect pentru crearea zonelor de cod care răspund ca efect al schimbării valorilor unui state.
const context = [];
function signal(value) {
const subscriptions = new Set();
const read = () => {
const observer = context[context.length - 1];
if (observer) subscriptions.add(observer);
return value;
};
const write = (newValue) => {
value = newValue;
for (const observer of subscriptions) {
observer.execute();
}
};
return [read, write];
}
function effect(fn) {
const effect = {
execute() {
context.push(effect);
fn();
context.pop();
}
};
effect.execute();
}
Dacă ar fi să vedem aceste funcții în acțiune, într-o pagină simplă de HTML, ar arăta în felul următor:
<h1 id="hook"></h1>
<button onclick="increment()">Increment</button>
<script>
const [data, setData] = signal(1);
effect(() => {
document.querySelector("#hook").textContent = data();
});
const increment = () => setData(data() + 1);
</script>
Adoptând Signals în majoritatea framework-urilor moderne, ne-am dat seama că sunt o idee bună, and we should do more of that. Astfel, în momentul de față există o propunere înaintată spre TC39 prin care campionii propunerii vor să introducă conceptul de Signals ca și un standard al limbajului JavaScript. Momentan, propunerea este de abia în primul stadiu (Stage 1), deci mai este cale lungă până va ajunge ca implementare în runtime, dar propunerea este bine scrisă și interesantă, deoarece își dorește să aducă un API pentru managementul state-ului reactiv la nivel de limbaj. De asemenea, propunerea este scrisă cu ideea ca frameworkurile să poată ulterior să construiască peste acest API, oferind interoperabilitate și suport pentru graficele de dependințe și mecanismul de auto-tracking, astfel încât frameworkul să poată să își rețină modul personal de implementare.
Viitorul sună bine! Cam toate frameworkurile moderne implementează în momentul de față o primitivă reactivă care se bazează pe conceptul de Signal, oferind programatorului ergonomie și simplitate, atunci când vine vorba de state management. Lucrurile nu se opresc aici, fiecare framework dezvoltă în continuare funcționalitatea proprie și aduce feature-uri noi. Prin propunerea de standardizare a Signalurilor la nivel de limbaj, putem să privim cu încredere la un viitor apropiat în care problema de state management nu va da așa de multe bătăi de cap dezvoltatorilor de aplicații din ecosistemul JavaScript.
de Ovidiu Mățan
de Ovidiu Mățan
de Ovidiu Mățan