Entwurf eines Standardisierungsvorschlags für JavaScript Signals
- Ein Dokument, das eine erste gemeinsame Richtung für Signals in JavaScript beschreibt, ähnlich den frühen Promises/A+-Bemühungen vor der Standardisierung von Promises durch TC39 in ES2015.
- Diese Initiative konzentriert sich auf die Koordination des JavaScript-Ökosystems; wenn diese Koordination erfolgreich ist, könnte auf Grundlage dieser Erfahrungen ein Standard entstehen.
- Mehrere Framework-Autoren arbeiten an einem gemeinsamen Modell zusammen, das den reaktiven Kern tragen kann.
- Der aktuelle Entwurf basiert auf Design-Input von Autoren/Maintainers von Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz u. a.
Hintergrund: Warum Signals?
- Um komplexe Benutzeroberflächen (UI) zu entwickeln, müssen Entwickler von JavaScript-Anwendungen Status effizient speichern, berechnen, invalidieren, synchronisieren und in die View-Schicht der Anwendung übertragen.
- Bei UIs geht es oft nicht nur um die Verwaltung einfacher Werte, sondern auch um das Rendern berechneter Zustände, die von anderen Werten oder Zuständen abhängen.
- Das Ziel von Signals ist es, die Infrastruktur zur Verwaltung solcher Anwendungszustände bereitzustellen, damit sich Entwickler stärker auf die Geschäftslogik statt auf wiederkehrende Details konzentrieren können.
Beispiel – VanillaJS-Zähler
- Es gibt eine Variable namens
counter, und jedes Mal, wenn sich diese Variable ändert, soll im DOM aktualisiert werden, ob der Zähler gerade oder ungerade ist.
- In Vanilla JS könnte das so aussehen:
let counter = 0;
const setCounter = (value) => {
counter = value;
render();
};
const isEven = () => (counter & 1) == 0;
const parity = () => isEven() ? "even" : "odd";
const render = () => element.innerText = parity();
// Simulate external updates to counter...
setInterval(() => setCounter(counter + 1), 1000);
- Dieser Code hat einige Probleme:
- Das Setzen von
counter ist umständlich und enthält viel Boilerplate.
- Der Zustand von
counter ist eng an das Render-System gekoppelt.
- Wenn sich
counter ändert, parity aber nicht (z. B. von 2 auf 4), werden unnötige Berechnungen und Renderings ausgeführt.
- Andere Teile der UI möchten möglicherweise nur bei
counter-Updates rendern.
- Andere UI-Teile, die nur von
isEven oder parity abhängen, können nicht aktualisiert werden, ohne direkt mit counter zu interagieren.
Einführung in Signals
- Die Abstraktion der Datenbindung zwischen Modell und View ist seit Langem ein Kernbestandteil von UI-Frameworks, obwohl weder JS noch die Web-Plattform einen solchen Mechanismus eingebaut haben.
- Innerhalb von JS-Frameworks und -Bibliotheken wurde viel mit verschiedenen Arten experimentiert, diese Bindungen darzustellen, und der Ansatz erstklassiger reaktiver Werte für Berechnungen, die aus Zustand oder anderen Daten abgeleitet werden und oft als „Signals“ bezeichnet werden, hat seine Stärke bewiesen.
- Mit einer Signal-API könnte das obige Beispiel so neu gedacht werden:
const counter = new Signal.State(0);
const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);
const parity = new Signal.Computed(() => isEven.get() ? "even" : "odd");
// A library or framework defines effects based on other Signal primitives
declare function effect(cb: () => void): (() => void);
effect(() => element.innerText = parity.get());
// Simulate external updates to counter...
setInterval(() => counter.set(counter.get() + 1), 1000);
Motivation für die Standardisierung von Signals
Interoperabilität
- Jede Signal-Implementierung hat ihren eigenen Mechanismus zur automatischen Nachverfolgung, was es schwierig macht, Modelle, Komponenten und Bibliotheken zwischen verschiedenen Frameworks zu teilen.
- Das Ziel dieses Vorschlags ist es, das reaktive Modell vollständig von der Rendering-View zu trennen, damit Entwickler beim Wechsel zu einer neuen Rendering-Technologie ihren Nicht-UI-Code nicht neu schreiben müssen oder gemeinsam nutzbare reaktive Modelle in JS entwickeln können, die in anderen Kontexten eingesetzt werden.
Performance/Speichernutzung
- Weniger Code zu übertragen, weil eine häufig verwendete Bibliothek eingebaut ist, kann grundsätzlich immer kleine potenzielle Performance-Vorteile bringen, aber Signal-Implementierungen sind in der Regel recht klein, daher wird nicht erwartet, dass dieser Effekt sehr groß ist.
Entwickler-Tools
- Bei der Nutzung bestehender Signal-Bibliotheken für die JS-Sprache ist es schwierig, Call Stacks durch Ketten berechneter Signals, Referenzgraphen zwischen Signals usw. nachzuverfolgen.
- Eingebaute Signals würden es der JS-Runtime und den Entwickler-Tools ermöglichen, eine verbesserte Unterstützung zur Inspektion von Signals bereitzustellen.
Zusätzliche Vorteile
Vorteile einer Standardbibliothek
- JavaScript hatte im Allgemeinen eine eher minimale Standardbibliothek, aber der Trend bei TC39 geht dahin, JS zu einer „batteries included“-Sprache mit einem hochwertigen Satz eingebauter Funktionen zu machen.
HTML/DOM-Integration (zukünftige Möglichkeit)
- Das W3C und Browser-Implementierer arbeiten derzeit daran, native Templates in HTML einzuführen.
- Um diese Ziele zu erreichen, werden letztlich reaktive Primitive in HTML benötigt.
Designziele für Signals
- Bestehende Signal-Bibliotheken unterscheiden sich im Kern nicht besonders stark.
- Dieser Vorschlag möchte auf ihrem Erfolg aufbauen, indem er wichtige Eigenschaften vieler Bibliotheken umsetzt.
Kernfunktionen
- Ein
Signal-Typ, der Zustand repräsentiert, also ein beschreibbares Signal.
- Ein berechneter/Memo/abgeleiteter
Signal-Typ, der von anderen Signals abhängt, lazy berechnet und gecacht wird.
- JS-Frameworks sollen ihr eigenes Scheduling durchführen können.
API-Skizze
- Eine erste Idee für die Signal-API sieht wie folgt aus. Dies ist nur ein früher Entwurf und wird sich voraussichtlich im Laufe der Zeit ändern.
namespace Signal {
// A read-write Signal
class State<T> implements Signal<T> {
// Create a state Signal starting with the value t
constructor(t: T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
// Set the state Signal value to t
set(t: T): void;
}
// A Signal which is a formula based on other Signals
class Computed<T> implements Signal<T> {
// Create a Signal which evaluates to the value returned by the callback.
// Callback is called with this signal as the this value.
constructor(cb: (this: Computed<T>) => T, options?: SignalOptions<T>);
// Get the value of the signal
get(): T;
}
// This namespace includes "advanced" features that are better to
// leave for framework authors rather than application developers.
// Analogous to `crypto.subtle`
namespace subtle {
// Run a callback with all tracking disabled (even for nested computed).
function untrack<T>(cb: () => T): T;
// Get the current computed signal which is tracking any signal reads, if any
function currentComputed(): Computed | null;
// Returns ordered list of all signals which this one referenced
// during the last time it was evaluated.
// For a Watcher, lists the set of signals which it is watching.
function introspectSources(s: Computed | Watcher): (State | Computed)[];
// Returns the Watchers that this signal is contained in, plus any
// Computed signals which read this signal last time they were evaluated,
// if that computed signal is (recursively) watched.
function introspectSinks(s: State | Computed): (Computed | Watcher)[];
// True if this signal is "live", in that it is watched by a Watcher,
// or it is read by a Computed signal which is (recursively) live.
function hasSinks(s: State | Computed): boolean;
// True if this element is "reactive", in that it depends
// on some other signal. A Computed where hasSources is false
// will always return the same constant.
function hasSources(s: Computed | Watcher): boolean;
class Watcher {
// When a (recursive) source of Watcher is written to, call this callback,
// if it hasn't already been called since the last `watch` call.
// No signals may be read or written during the notify.
constructor(notify: (this: Watcher) => void);
// Add these signals to the Watcher's set, and set the watcher to run its
// notify callback next time any signal in the set (or one of its dependencies) changes.
// Can be called with no arguments just to reset the "notified" state, so that
// the notify callback will be invoked again.
watch(...s: Signal[]): void;
// Remove these signals from the watched set (e.g., for an effect which is disposed)
unwatch(...s: Signal[]): void;
// Returns the set of sources in the Watcher's set which are still dirty, or is a computed signal
// with a source which is dirty or pending and hasn't yet been re-evaluated
getPending(): Signal[];
}
// Hooks to observe being watched or no longer watched
var watched: Symbol;
var unwatched: Symbol;
}
interface Options<T> {
// Custom comparison function between old and new value. Default: Object.is.
// The signal is passed in as the this value for context.
equals?: (this: Signal<T>, t: T, t2: T) => boolean;
// Callback called when isWatched becomes true, if it was previously false
[Signal.subtle.watched]?: (this: Signal<T>) => void;
// Callback called whenever isWatched becomes false, if it was previously true
[Signal.subtle.unwatched]?: (this: Signal<T>) => void;
}
}
Signal-Algorithmus
- Beschreibt die Algorithmen, die für jede gegenüber JavaScript exponierte API implementiert werden.
- Dies kann als frühe Spezifikation verstanden werden und zielt darauf ab, eine möglichst präzise Menge an Semantik festzulegen, obwohl weiterhin sehr offene Änderungen möglich sind.
Meinung von GN⁺
- Der Standardisierungsvorschlag für JavaScript Signals zielt darauf ab, die Interoperabilität zwischen Frameworks zu verbessern und es Entwicklern zu erleichtern, reaktive Programmierung umzusetzen.
- Der Vorschlag ist ein Versuch, die Kernfunktionen verschiedener bestehender Signal-Bibliotheken zu standardisieren, und könnte Entwicklern ein konsistentes Programmiermodell bieten.
- Das Signal-Konzept kann nicht nur in der UI-Entwicklung, sondern auch in Nicht-UI-Kontexten nützlich eingesetzt werden, insbesondere um in Build-Systemen unnötige Rebuilds zu vermeiden.
- Die vorgeschlagene API bietet Framework-Entwicklern nützliche Werkzeuge, mit denen sich voraussichtlich bessere Performance und effizienteres Speichermanagement erreichen lassen.
- Damit sich diese Technologie breit durchsetzt, sind jedoch mehr Prototyping und Feedback aus der Community nötig, und ihre Wirkung muss durch die Integration in reale Anwendungen belegt werden.
- Frameworks wie React, Vue und Svelte verfügen bereits über eigene reaktive Systeme; daher werden auch Kompatibilität und Integrationsstrategien mit diesen Frameworks wichtige Aspekte sein.
1 Kommentare
Hacker-News-Kommentar
Beispiel: Vanilla JS vs. Signals
isEvenoder der Parität abhängen, muss womöglich der gesamte Ansatz geändert werden.Promises und die Veränderung von JavaScript
new Promisewürde schreiben müssen, tatsächlich habe ich es aber fast nie verwendet..thenverwendet, was die Schnittstelle zu verschiedenen Drittanbieter-Bibliotheken vereinfacht.Signals als Teil der Sprache
Einsatz von Events in Anwendungen
window.dispatchEventundwindow.addEventListenerwerden Events ausgelöst und abonniert.Schwierigkeiten bei DOM-Statusverwaltung und Updates
Promises und asynchrone Programmierung
S.js und Signals
Signals ähnlich wie MobX
Ein Framework zur Standardbibliothek hinzufügen
Verständnis und Probleme des Signal-Vorschlags
effectÄnderungen der Parität erkennt und ob diese Lambda-Funktion bei jeder Signaländerung aufgerufen wird.