3 Punkte von GN⁺ 2024-04-01 | 1 Kommentare | Auf WhatsApp teilen

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

 
GN⁺ 2024-04-01
Hacker-News-Kommentar
  • Beispiel: Vanilla JS vs. Signals

    • Bin ich der Einzige, der das Vanilla-JS-Beispiel lesbarer und angenehmer zu bearbeiten findet?
      • Die Einrichtung wirkt komplex und scheint viel Boilerplate zu enthalten.
      • Wenn sich der Zählerwert ändert, können unnötige Berechnungen und Renderings stattfinden.
      • Wenn andere Teile der UI nur bei Zähler-Updates gerendert werden sollen, muss möglicherweise die Art der Zustandsverwaltung geändert werden.
      • Wenn andere Teile der UI nur von isEven oder der Parität abhängen, muss womöglich der gesamte Ansatz geändert werden.
  • Promises und die Veränderung von JavaScript

    • Anfangs hatte ich Sorge, dass ich oft new Promise würde schreiben müssen, tatsächlich habe ich es aber fast nie verwendet.
    • Stattdessen habe ich häufig .then verwendet, was die Schnittstelle zu verschiedenen Drittanbieter-Bibliotheken vereinfacht.
    • Wenn der Signal-Vorschlag für reaktive UI-Frameworks einen ähnlichen Effekt hat, bin ich dafür.
  • Signals als Teil der Sprache

    • Signals müssen kein Teil der Sprache sein; als Bibliothek reichen sie völlig aus.
    • Zu glauben, dass die von heutigen JS-UI-Bibliotheken entworfenen Signals gut genug seien, um Teil der Sprache zu werden, wirkt anmaßend.
    • Jeden Trend in die Sprach-Runtime aufzunehmen, erscheint kurzsichtig.
  • Einsatz von Events in Anwendungen

    • In der gesamten Anwendung werden Events verwendet, um Signale zu senden.
    • Über window.dispatchEvent und window.addEventListener werden Events ausgelöst und abonniert.
  • Schwierigkeiten bei DOM-Statusverwaltung und Updates

    • Ich versuche zu verstehen, warum sich Menschen seit Jahrzehnten mit Zustandsverwaltung und DOM-Updates schwertun.
    • Es wirkt befremdlich, dass einfache DOM-Funktionen so kompliziert gemacht werden.
  • Promises und asynchrone Programmierung

    • Promises sind ein Erfolgsbeispiel, aber ohne async/await hätten sie wohl nicht standardisiert werden müssen.
    • Ich frage mich, wie verschiedene Bibliotheksautoren über diesen Vorschlag denken.
  • S.js und Signals

    • Ich mag Signals und bevorzuge sie beim Bauen von UIs gegenüber anderen Grundbausteinen.
    • Ich denke jedoch nicht, dass sie in die JavaScript-Sprache aufgenommen werden sollten.
  • Signals ähnlich wie MobX

    • MobX ist mein bevorzugtes JS-Effekt-System.
    • Es wird ein Codebeispiel in der MobX-Variante gezeigt.
  • Ein Framework zur Standardbibliothek hinzufügen

    • Das ist ähnlich, als würde man vorschlagen, das aktuell bevorzugte Framework zur Standardbibliothek hinzuzufügen.
  • Verständnis und Probleme des Signal-Vorschlags

    • Ich habe Schwierigkeiten, die Beispiele des Signal-Vorschlags zu verstehen.
    • Es gibt Fragen dazu, wie die Funktion effect Änderungen der Parität erkennt und ob diese Lambda-Funktion bei jeder Signaländerung aufgerufen wird.
    • Die Signal-Idee ist plausibel, aber in komplexen Anwendungen könnte das Nachverfolgen von Events schwierig werden.