3 Punkte von GN⁺ 2024-04-01 | 1 Kommentare | Auf WhatsApp teilen
  • Der JavaScript-Signals-Vorschlag von TC39 ist eine frühe Initiative, reaktive Primitive zur effizienten Nachverfolgung von UI-Zustand und berechnetem Zustand zu standardisieren; aktuell liegt er als Entwurf auf Stage 1 vor
  • Der Vorschlag konzentriert sich weniger auf eine Oberflächen-API für direkte Nutzung durch Anwendungsentwickler, sondern auf die Kernsemantik eines Signal-Graphen und automatische Tracking-Mechanismen, die Frameworks gemeinsam nutzen können
  • Signal.State, Signal.Computed und Signal.subtle.Watcher sind die Kern-APIs; Berechnungen zielen auf lazy evaluation, Caching, automatische Abhängigkeitsverfolgung und glitch-free Ausführung ab
  • Ziel eingebauter Signals ist es, die Interoperabilität zwischen Frameworks wie Angular, Ember, MobX, Preact, Qwik, Solid, Svelte und Vue zu erhöhen und Unterstützung für Debugging und Performance-Analyse in DevTools zu ermöglichen
  • Die Vorschlagsgruppe will vor Stage 2 mehrere produktionsreife Polyfills, Framework-Integrationen, Validierung in großen Anwendungen und Performance-Benchmarks durchlaufen; bis zur Standardisierung kann es mindestens 2–3 Jahre oder länger dauern

Einordnung des JavaScript-Signals-Vorschlags

  • JavaScript Signals werden als Stage-1-Vorschlag im TC39-Prozess vorgestellt
  • Das aktuelle Dokument ist, ähnlich wie Promises/A+ vor der Standardisierung von Promise in ES2015, ein Versuch, eine gemeinsame Richtung im JavaScript-Ökosystem abzustimmen
  • Ein Polyfill zum direkten Experimentieren ist verfügbar
  • Die Champions des Vorschlags und die ursprünglichen Autoren haben den aktuellen Entwurf auf Basis von Design-Input aus mehreren Frameworks und Libraries erstellt

Das Problem, auf das die Standardisierung abzielt

  • Komplexe UIs müssen Werte speichern, berechnen, invalidieren, synchronisieren und in die View-Schicht pushen; Signals sollen dafür eine State-Management-Infrastruktur bereitstellen
  • In einem Vanilla-JS-Beispiel sind counter, isEven, parity und render direkt miteinander verflochten, wodurch folgende Probleme entstehen
    • Zustand und Rendering-System sind stark gekoppelt
    • Auch wenn sich parity nicht ändert, etwa wenn counter von 2 auf 4 wechselt, entstehen unnötige Berechnungen und Renderings
    • Wenn andere UI-Teile nur Teile von counter, isEven oder parity abonnieren wollen, wird die manuelle Verwaltung von Subscriptions und deren Aufhebung komplex
    • Fügt man pub/sub über mehrere Ebenen hinzu, wachsen Boilerplate und Subscription-Bookkeeping, und das Risiko von Memory Leaks steigt
  • Ein auf Signals basierendes Beispiel behandelt Werte, Berechnungen und Seiteneffekte mit Signal.State und Signal.Computed auf eine einheitliche Weise
    • Manuelle Subscriptions sind nicht nötig
    • Ein berechnetes Signal findet automatisch die Signals, von denen es abhängt
    • Berechnungen werden nur ausgeführt, wenn der Wert explizit angefordert wird
    • Ein berechnetes Signal cached den letzten Wert

Kern-API des Vorschlags

  • Signal<T> wird als lesbarer Wert mit get(): T definiert
  • Signal.State<T> ist ein beschreibbares Signal
    • Der Konstruktor nimmt einen Initialwert und Optionen entgegen
    • Werte werden mit get() gelesen und mit set(t) geändert
  • Signal.Computed<T> ist ein berechnetes Signal auf Basis anderer Signals
    • Es wird aus dem Rückgabewert eines Callbacks berechnet
    • Abhängigkeiten werden automatisch verfolgt
    • Der Wert wird lazy berechnet und gecached
  • Signal.subtle enthält fortgeschrittene APIs, die eher für Framework-Autoren oder DevTools-Implementierungen gedacht sind
    • untrack(cb) erlaubt das Lesen von Signals ohne Tracking
    • currentComputed() gibt das aktuell getrackte computed Signal zurück
    • introspectSources, introspectSinks, hasSinks und hasSources sind APIs zur Beobachtung des Graphen
    • Watcher ist die Grundlage, um Änderungen an Signals zu erkennen und Effects sowie Scheduling auf Framework-Ebene zu implementieren
  • SignalOptions<T> unterstützt eine benutzerdefinierte Vergleichsfunktion equals sowie watched- / unwatched-Hooks

Funktionsweise und Ausführungsmodell

  • Ein Signal repräsentiert eine Datenzelle, die sich über die Zeit ändern kann, und ist entweder state oder computed
  • Ein berechnetes Signal zeichnet automatisch die während der Ausführung gelesenen Signals auf und prüft beim späteren Lesen, ob sich frühere Abhängigkeiten geändert haben
  • Berechnungen sind pull-based
    • Auch wenn sich eine Abhängigkeit ändert, wird nicht sofort neu berechnet
    • Bei Bedarf wird neu berechnet, wenn jemand mit .get() liest
  • Schreibvorgänge auf State-Signals werden synchron reflektiert
    • Wird nach .set() ein computed Signal gelesen, das von diesem Wert abhängt, wird es bei Bedarf sofort neu berechnet
    • Eingebautes Batching gibt es nicht
  • Der notify-Callback eines Watchers kann während .set() synchron ausgeführt werden
    • Während notify dürfen jedoch keine Signals gelesen oder geschrieben werden
    • Tatsächliche Lese- und Schreiboperationen müssen anschließend geplant werden
  • Wenn der Callback eines computed Signals eine Exception wirft, wird auch diese Exception wie ein Wert gecached und beim erneuten Lesen des Signals erneut geworfen

Motivation für die Standardisierung

  • Die Signal-Implementierungen der einzelnen Frameworks besitzen jeweils eigene automatische Tracking-Mechanismen, wodurch es schwierig ist, Modelle, Komponenten und Libraries zwischen Frameworks zu teilen
  • Ziel des Vorschlags ist es, reaktive Modelle von Rendering-Views zu trennen
    • Entwickler sollen Nicht-UI-Code nicht neu schreiben müssen, wenn sie die Rendering-Technologie wechseln
    • Es soll möglich werden, reaktive Modelle in JavaScript zu erstellen, die in mehreren Kontexten gemeinsam genutzt werden können
  • In Bezug auf Performance und Speicher weist der Vorschlag darauf hin, dass eine eingebaute Implementierung möglicherweise um einen kleinen konstanten Faktor effizienter ist als JS-Implementierungen, die Engine den Algorithmus aber nicht auf magische Weise verändert
  • Aus DevTools-Sicht könnten eingebaute Signals folgende Informationen besser sichtbar machen
    • Call Stack von computed-Signal-Ketten
    • Referenzgraph zwischen Signals
    • Abhängigkeitsbeziehungen, die für das Debugging der Speichernutzung nötig sind
  • Bei Aufnahme in die Standardbibliothek werden zudem Nebeneffekte wie kleinere Bundle-Größen, höhere Stabilität und Qualität sowie ein gemeinsames Vokabular zwischen Projekten erwartet

Designziele und Einschränkungen

  • Zu den Kernfunktionen gehören beschreibbare Signals, berechnete Signals, Reaktionen auf dirty-Zustände, Framework-eigenes Scheduling, untrack und die Komposition mehrerer Codebases
  • Berechnete Signals zielen auf glitch-free ab
    • Um unnötige Berechnungen zu vermeiden, werden potenziell dirty Bereiche des Graphen topologisch sortiert ausgeführt
    • Doppelte Berechnungen sollen eliminiert werden
  • Damit Frameworks ihr eigenes Scheduling durchführen können, wird kein eingebautes erzwungenes Scheduling im Stil von Promises aufgenommen
  • Um Missbrauch synchroner Reaktions-Callbacks zu verhindern, sind Lesen und Schreiben von Signals innerhalb von notify eines Watchers verboten
  • untrack wird als unsicherer Escape Hatch behandelt
    • Wenn ein ohne Tracking gelesenes Signal das Berechnungsergebnis beeinflusst, wird das computed Signal bei Änderungen dieses Signals möglicherweise nicht aktualisiert
  • Die API priorisiert eine Grundlage für Framework-Implementierungen und ist nicht speziell auf Ergonomie für normale App-Entwickler ausgelegt

Effects und Watcher

  • Der Vorschlag enthält keine eingebaute Funktion wie effect()
  • Effect-Scheduling ist mit Rendering-Zyklen, Disposal und Ownership-Management von Frameworks verflochten und wird daher nicht direkt durch eine JavaScript-Standard-API gelöst
  • Stattdessen bietet Signal.subtle.Watcher eine Low-Level-Grundlage für die Implementierung von Effects
    • Wenn sich Abhängigkeiten eines beobachteten Signals ändern, wird notify aufgerufen
    • Mit getPending() lassen sich noch dirty Signals prüfen
    • Effects, die ein Dispose benötigen, müssen mit unwatch bereinigt werden
  • Signals, die ein Watcher beobachtet, können weiterleben, solange ihr interner State erreichbar ist; beim Bereinigen eines Effects ist daher ein Aufruf von Watcher.prototype.unwatch nötig

Funktionen, die im aktuellen Entwurf fehlen

  • Async ist derzeit nicht Teil des Modells
    • Signals werden stets als synchron auswertbare Werte behandelt
    • Der Loading-Zustand lässt sich teilweise über Exceptions modellieren; Diskussionen zu Verbesserungen gibt es in Issue #30
  • Transactions sind ebenfalls nicht enthalten
    • Wenn bei einem Screen-Übergang ein „from“- und ein „to“-Zustand gleichzeitig gehalten werden sollen, entsteht das Problem, den Zustand des Signal-Graphen zu forken
    • Die zugehörige Diskussion findet sich in Issue #73
  • Einige convenience methods fehlen ebenfalls im aktuellen Entwurf
  • Diese fehlenden Funktionen wurden wegen mangelnder Einigung zwischen Frameworks und wegen möglicher Workarounds auf höheren Ebenen ausgeschlossen, könnten nach Prototypen aber erneut geprüft werden

Entwicklungsplan und Standardisierungszeitplan

  • Dieser Vorschlag steht im April 2024 auf der TC39-Agenda für Stage 1; das Dokument erklärt, dass er derzeit praktisch eher als Stage 0 betrachtet werden kann
  • Vor einem Vorschlag für Stage 2 sind folgende Arbeiten geplant
    • Entwicklung mehrerer produktionsreifer Polyfill-Implementierungen
    • Tests in verschiedenen Frameworks und Bestehen von Tests im Stil von test262
    • Performance-Validierung anhand eines thorough signal/framework benchmark set
    • Integration der vorgeschlagenen API in mehrere repräsentative JS-Frameworks und einige große Anwendungen
    • Verständnis der Erweiterbarkeit der API und Entscheidung über deren Aufnahme
  • Die Vorschlagsgruppe will konservativ vorgehen, um zu vermeiden, dass eine falsche Form von Signals zu schnell standardisiert wird
  • Die FAQ erwartet, dass es mindestens 2–3 Jahre dauern wird, bis Standard-Signals browserübergreifend ohne Polyfill nutzbar sind
  • Der aktuelle Polyfill ist nutzbar, es wird aber empfohlen, sich nicht auf seine Stabilität zu verlassen, da sich die API im Review-Prozess ändern kann

In der FAQ zusammengefasstes Nutzungsmodell

  • Eingebaute Signals sind unabhängig von der Rendering-Technologie
    • Es werden Formen wie Preact mit VDOM, Solid mit native DOM und Vue mit gemischtem Ansatz als möglich beschrieben
  • App-Entwickler sollten Signals in der Regel über ein Framework nutzen
    • Das Framework verwaltet Watcher, untrack, Ownership, Disposal und das Scheduling des DOM-Renderings
  • Sie können zusammen mit SSR, Hydration und Resumability verwendet werden
    • Qwik nutzt Signals zusammen mit diesen Eigenschaften, und die Vorschlagsseite geht davon aus, dass sich die resumable Signals von Qwik als Kombination aus State und Computed modellieren lassen
  • Signals und Proxy ergänzen sich gegenseitig
    • Proxy fängt flache Objektoperationen ab, Signals koordinieren den Abhängigkeitsgraphen von Datenzellen
    • Legt man Signals hinter einen Proxy, lässt sich eine nested reactive structure ergonomischer gestalten
  • Signals sind keine Streams, sondern Zellen, die den aktuellen Wert repräsentieren
    • Wenn zweimal hintereinander in ein State-Signal geschrieben wird und sonst nichts passiert, ist der erste Schreibvorgang für computed Signals oder Effects möglicherweise nicht sichtbar
    • Das Dokument betrachtet dies als Eigenschaft auf der Gegenseite von glitch-free Ausführung und hält andere Konstrukte wie async iterables oder observables für Streams für besser geeignet

1 Kommentare

 
GN⁺ 2024-04-01
Meinungen auf Hacker News
  • Bin ich der Einzige, der das Gefühl hat, dass das reine JavaScript-Beispiel im Gegenteil leichter zu lesen und zu handhaben ist?
    Zwar heißt es, „das Setup sei voller Rauschen und Boilerplate“, aber das Signals-Beispiel wirkt genauso laut und voller Boilerplate und fügt sogar noch ein neues Konzept hinzu, das für Einsteiger schwer zu verstehen ist.
    Dass „sich parity nicht ändert, wenn counter von 2 auf 4 wechselt, aber trotzdem unnötig berechnet und gerendert wird“, klingt nach voreiliger Memoization.
    Wenn andere Teile der UI passend zu counter-Updates rendern sollen, ist dieses Strawman-Beispiel tatsächlich nicht geeignet; dann kann man andere Ansätze wie signals, Event-Handling oder einen zentralen State Store (à la Redux) verwenden.
    Wenn andere Teile der UI nur von isEven oder parity abhängen, könnte man bei der Kernstruktur der App den Ansatz ändern, aber meistens ist das nicht der Fall. Auch dass „eine render-Funktion, die nur von parity abhängt, wissen muss, dass sie counter abonnieren soll“, ist nicht unbedingt eine unzumutbare Last; reine Berechnungsfunktionen haben den Vorteil, dass ihre Eingaben leicht zu erkennen sind.

    • Ich verstehe nicht, warum das als voreilige Memoization gesehen wird. Das ist lediglich ein auf eine einfache Funktion reduziertes Beispiel, und es ist schwer zu glauben, dass sich Leute solche Anwendungsfälle ausgedacht haben, ohne sie jemals wirklich gebraucht zu haben.
      Der Versuch, signals zu standardisieren, ein Konzept, das in der UI-Entwicklung immer häufiger genutzt wird, ist lobenswert. Unabhängig von Detaildebatten darüber, wie viel Boilerplate anfällt oder ob man ein Event-System selbst bauen muss: Wenn mehrere Frameworks signals verwenden, kann es dafür Gründe geben, und ein Standardisierungsversuch lohnt sich, auch wenn er Zeit braucht.
    • Stimme zu. Allerdings passt der Kontext deutlich besser, wenn man sich die signal-Dokumentation von Preact ansieht.
      https://preactjs.com/guide/v10/signals
      In Preact wird, wenn ein signal als props oder context den Tree hinuntergereicht wird, nur die signal-Referenz übergeben; Komponenten betrachten das signal und nicht den Wert, sodass ein Update des signals die Komponente unter Umständen nicht neu rendert. Tatsächlich kann es direkt zu der Komponente im Tree gehen, die auf .value zugreift.
      Außerdem verfolgt ein signal, wann auf seinen Wert zugegriffen und wann er aktualisiert wird; in Preact wird eine Komponente automatisch neu gerendert, wenn sich der Wert eines signals ändert, auf dessen .value innerhalb der Komponente zugegriffen wurde.
    • Da Reaktivität nicht in JavaScript eingebaut ist, entstehen zwangsläufig Abstraktionskosten, wenn man Reaktivität hinzufügt. Sie ist dafür gedacht, bei Bedarf genutzt zu werden, nicht unbedingt als Standardmethode für den Umgang mit State.
      Aus Erfahrung liegt der große Vorteil darin, reaktiven State zu modularisieren. Im imperativen Stil braucht man zusätzlichen State, um Änderungen nachzuverfolgen, und Modularität wird über Abstraktion erreicht. Man sollte es nur verwenden, wenn man es braucht.
      Ein einfaches und zugleich anwendbares Beispiel zu erstellen, ist eine Frage der Balance. Fälle, in denen Reaktivität klar von Vorteil ist, sind meist komplexer und daher schwerer zu zeigen als einfache, aber weniger anwendbare Beispiele.
    • Die Art, wie das erklärt wird, lässt sich verbessern. In kleinen Beispielen ist das Problem schwer zu erkennen; es zeigt sich erst in größerem Maßstab. PRs willkommen.
    • Dass ab einem bestimmten Komplexitätsschwellenwert ein Designwechsel nötig wird, ist etwas, das man vermeiden sollte. Der reine JS-Ansatz hat Grenzen bei der Skalierung im Hinblick auf die Komplexität des State-Graphen, und das eigentliche Problem ist nicht die Benutzbarkeit vor oder nach diesem Schwellenwert, sondern dass sich die Benutzbarkeit in dem Moment, in dem man ihn überschreitet, sprunghaft ändert.
  • Als Promises zu JavaScript hinzugefügt wurden, hatte ich Vorbehalte, weil ich befürchtete, überall new Promise schreiben zu müssen.
    In der Praxis kann ich die Male, in denen ich new Promise direkt geschrieben habe, an zwei Händen abzählen. Stattdessen verwende ich, besonders beim Umgang mit Third-Party-Libraries, viel häufiger .then.
    Letztlich bestand der alltägliche Effekt der Aufnahme von Promise in JavaScript darin, dass es für verschiedenste spezielle Verhaltensweisen und Funktionen von Third-Party-Libraries ein ziemlich einfaches, weitgehend robustes und fast universelles Interface bot. Ob beim Lesen von Dateien, bei API-Requests oder bei Ausgaben aus Build-Schritten: Mit .then(res => …) fühlt es sich so an, als sei man schon halb bei etwas, das funktioniert.
    Wenn dieser Signal-Vorschlag in der kambrischen Explosion reaktiver UI-Frameworks eine ähnliche Rolle spielen kann, bin ich dafür. Darüber hinaus könnte er auch dabei helfen, Reaktivität über die UI hinaus auszuweiten. Ich habe mir oft State-Trees für inkrementelle Neuberechnung für Dinge vorgestellt, die kein UI-State sind.

    • Ich habe Promises vor allem als Grundlage für async/await gesehen, und das war es, was die Lebensqualität wirklich deutlich erhöht hat. In der Praxis muss man selten direkt new Promise schreiben.
      Das frühe .then von Promise war gegenüber verschachtelten Delegates eine große Verbesserung und für einfache Chains in Ordnung; aber wenn man bedingt unterschiedliche Promises verkettet, je nach Chain unterschiedliche Fehlerbehandlung hat oder Early Returns braucht, kann der Code deutlich schwerer zu lesen und zu handhaben werden.
      Mit async/await kann man Aufrufe so schreiben, als wären sie keine Promises, leicht try/catch um bestimmte Promise-Aufrufe legen und Early Returns auf natürliche Weise verwenden.
  • Ich verstehe nicht, warum das Teil der Sprache werden sollte. Es ist als Library möglich, und solche Libraries gibt es bereits. Weil es klein ist, ist es keine große Belastung, es in den Code aufzunehmen; die Aufnahme in die Sprache sollte kein Selbstzweck sein
    Zu glauben, die aktuellen JS-UI-Libraries hätten Signals so gut entworfen, dass sie Teil der Sprache werden müssten, ist anmaßend. Für Signals gibt es viele Implementierungen mit unterschiedlichen Trade-offs, und keine davon verdient einen besonderen Platz in der JavaScript-Spezifikation
    Bevor diese Libraries Signals nutzten, verwendeten sie ein virtuelles DOM. Glücklicherweise ist das virtuelle DOM nicht Teil von JS geworden – was ist bei Signals anders? Nichts. Das Argument für eine Standardisierung ist sogar schwächer als damals beim virtuellen DOM
    Sollen wir wirklich alles, was gerade populär ist, in eine Runtime stapeln, aus der sich unerwünschte Features praktisch nicht mehr entfernen lassen, ohne das Web kaputtzumachen? Ziemlich kurzsichtig

    • Da ist etwas dran. Man will nichts Falsches, aber man will das Richtige
      Reaktive UI hat gewonnen. Selbst bei kleinen Anwendungen ist die Explosion der Komplexität beim State Management der Kern dessen, was reines JS schwer nutzbar macht. Für mich ist jedes reaktive Framework besser als reines JS; dann fehlt vielleicht tatsächlich ein Baustein
      Nach inzwischen etwa 10 Jahren ist es an der Zeit, über eine Grenze nachzudenken, die sich standardisieren lässt. Wenn es, wie bei Promises, richtig gemacht wird, kann es die Komplexität sehr häufiger Use Cases senken
      Eine bessere Bewertung ist die Frage: „Würden bestehende reaktive Frameworks diesen Vorschlag nutzen?“ Wenn nicht, sollte man ansehen, warum, was fehlt, was überflüssig ist und was man von UI und Reaktivität in anderen Sprachen lernen kann. Es lohnt sich, die verstreuten Erfahrungen zu destillieren
    • Ein guter Grund, Signals zu standardisieren, ist, dass Debugging nach einem Albtraum aussieht. Man stelle sich einen tiefen Baum berechneter Signals vor, die sich gegenseitig kaskadenartig auslösen, und man muss den Startpunkt dieser Kettenreaktion finden. Bei einer Standardisierung könnten Developer Tools darum herum gebaut werden
    • Dasselbe lässt sich über den Großteil einer Standard Library sagen. Wie in der Motivation erwähnt, gibt es aber eine Tendenz, die vergleichsweise kleine Standard Library von JS zu erweitern, damit man nicht für jede häufige Aufgabe ein Package importieren muss
      Über die Notwendigkeit kann man streiten, aber wenn man die Standard Library erweitert, halte ich es für einen guten Ansatz, auf populäre Dinge zu schauen
      Signals sind kein Ersatz für das virtuelle DOM
    • Erinnert an den Observable-Vorschlag
  • Wenn man etwas an die gesamte Anwendung signalisieren muss, nutzt man Events
    window.dispatchEvent(new Event('counterChange'));
    Und jeder Teil der Anwendung, der reagieren möchte, kann sich so subscriben
    window.addEventListener('counterChange', () => { ... do something ... });
    Was ist an diesem Ansatz problematisch?

    • Historisch gesehen ist genau dieses Beispiel der Grund, warum sich das Web zu jQuery entwickelt hat und sich von dort in die Welten von Angular und React aufgespalten hat
      Event Handling wird sehr leicht chaotisch. Wer tiefer einsteigen will, sollte sich Event Bubbling und Propagation ansehen
      Große Anwendungen brauchen robustes Event Handling, und das ist ein heute weniger sichtbarer Vorteil von Frameworks wie Angular und Vue
      Man wird die Standard-Event-Handling-APIs nicht ohne Framework direkt verwenden wollen. Wenn man für viele Elemente Hinzufügen, Löschen, Kopieren, Auslösen, Entfernen, einmaliges Auslösen usw. handhabt, kann es zu schwerwiegenden unerwünschten Nebeneffekten kommen
    • Laut Artikel verursachen Event Publisher/Observables bei mehrfachen Aufrufen unnötige Arbeit
      Der Unterschied zu Signals besteht darin, dass der Ergebniswert erst berechnet wird, wenn der endgültige Consumer den Wert liest. Der Zeitpunkt des tatsächlichen Schreibens in ein Signal wird von der Planung asynchroner Render-Updates getrennt, und die von Beobachtern ausgeführte Berechnungskette wird während des Renderns nur einmal ausgeführt
      Zwischenwerte, die per Signal gesendet wurden, verschwinden; daher ist es schwierig, darin viel Interessantes zu tun. Im Grunde handelt es sich eher um eine höhere Abstraktionsschicht zur Koordination des Rendering-Zyklus
    • Signals sind letztlich auch Publish/Subscribe, aber mit einer angenehmeren API, weil Listener automatisch hinzugefügt und entfernt werden
      Auch die Performance kann besser sein. Wenn man zum Beispiel eine Berechnung hat, die von zwei Werten abhängt, etwa result = a ? b : 0, muss bei falschem a nicht neu berechnet werden, wenn sich b ändert. Bei Signals passiert das automatisch; mit traditionellem Publish/Subscribe braucht man dafür ziemlich viel Code
    • Ich habe dieses Pattern über 10 Jahre lang verwendet. Das Schwierige daran ist, dass mit der Zeit ein Listener ein anderes Event triggern kann und ein weiteres Event wieder zur ersten Routine zurückführt, sodass eine endlose Listener-Schleife entsteht
      Es ist auch schwer zu garantieren, dass nicht irgendein Listener eine solche Kette von Triggern erzeugt
    • Dieser Ansatz hat alle Nachteile der Publish/Subscribe-Architektur, die der Vorschlag hervorhebt
  • Seit Jahrzehnten versuche ich zu verstehen, warum sich Leute mit State-Tracking und DOM-Updates so schwertun
    Natürlich braucht es ein wenig Disziplin, aber es fühlt sich viel einfacher an als die Lösungen, die alle paar Jahre auftauchen. Backbone, Knockout, Angular, React, Änderungen an der Sprache selbst und so weiter – vielleicht ist meine Denkweise einfach grundlegend anders
    Das zeigt sich schon an den Funktionsnamen. Ein Update von innerText wird „render“ genannt, obwohl es gar nicht wirklich rendert. Wenn überhaupt, rendert der Browser, und das gilt genauso für alles andere, was mit Painting zu tun hat. Es wirkt wie ein verzweifelter Versuch, eine der einfachsten DOM-Funktionen kompliziert zu machen, und das irritiert mich wirklich

    • Bei einfachen Anwendungen ist es leicht
      Wenn es komplexer wird, ist es nicht mehr leicht
    • Bei der Aussage „ein wenig Disziplin ist nötig“ habe ich stark das Gefühl, dass man früher als ASM-Programmierer auf diese verrückten portablen C-Programmierer geschimpft hätte und später als C-Programmierer auf die verrückten speichersicheren Java-Programmierer
      Fortschritt in der Programmierung kann man als Prozess sehen, bei dem rituelle, strenge Disziplin, die für gute Ergebnisse nötig ist, beseitigt wird
      Das heißt nicht, dass React die nächste Evolutionsstufe ist, aber signals sind eindeutig ein Schritt in die richtige Richtung
    • Jahrzehnte sind eine lange Zeit. Man wird sich erinnern, wie kompliziert DOM-Updates je nach Browser waren
      Das DOM mit dem Datenzustand zu synchronisieren ist an sich nicht allzu schwierig, aber das mit 60fps und sehr guter Performance zu tun, ist enorm schwierig. Besonders dann, wenn man eine API bauen will, die nicht leckt und zugleich nicht zu umständlich ist
      Änderungen in einen lebenden DOM-Baum zu übersetzen und dort abzubilden, kann ehrlich gesagt schwieriger sein, als wie in einem Spiel Pixel auf ein canvas zu zeichnen
    • Wir bauen eine FX-Trading-Anwendung mit mehreren Hunderttausend Zeilen Code. Sie ersetzt eine schwere Desktop-App, mit 20–30 Entwicklern und mehreren Kunden, die jeweils eigene Entwickler haben. Wenn man das ohne Framework versuchen will: viel Glück
    • Sehe ich ganz genauso. Ich habe auch sehr komplexe und interaktionsreiche SPAs entwickelt, aber ich bin dem Problem, das solche Dinge angeblich lösen, noch nicht begegnet
  • Promises sind ein gutes Erfolgsbeispiel, aber ohne async/await hätten sie nicht unbedingt standardisiert werden müssen
    Der aktuelle Entwurf soll auf Designs von Autoren/Maintainern von Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz und anderen basieren; ich frage mich, wie die Autoren bestehender Libraries diesen Vorschlag sehen. Interessant ist auch, dass React nicht auf der Liste steht
    Signals sind Channels ein wenig ähnlich, unterscheiden sich aber dadurch, dass sie Broadcast statt Single-Receiver sind. Es wäre cool, wenn man das nutzen könnte, damit Web Worker statt über onMessage-Callbacks über Channels kommunizieren. Besonders wenn man, wie in Go, über signals/channels/promises select ausführen könnte, hätte das syntaktische Vorteile gegenüber der Verwaltung mehrerer gleichzeitiger Messaging-Mechanismen mit Callbacks. Zum Beispiel indem man signals in Promise.any einbeziehen kann

    • Ich widerspreche entschieden der Aussage, dass sie „ohne async/await nicht hätten standardisiert werden müssen“
      x instanceof Promise funktioniert einfach nicht. Wenn die then-Methode meiner Library einen catch-Callback annimmt, die einer anderen Library aber nicht, dann interoperieren sie stillschweigend nicht, und es gibt keine Möglichkeit, das zu erkennen. Wann wird finally ausgeführt? Welche Erwartungen darf man daran haben, wie asynchron Callbacks ausgeführt werden?
      Ohne Standard müsste jede Library, die Promises verwendet, ihr eigenes Polyfill mitbringen, weil man dem vorhandenen nicht vertrauen kann. Und Promises anderer Libraries kann man auch nicht wirklich konsumieren, weil man nicht darauf vertrauen kann, dass sie sich wie erwartet verhalten
      Das ist keine Vermutung, sondern war jahrelang tatsächlich so, und es war eine Hölle, die viele Leute ertragen mussten
    • Es gibt auch Vorteile der Standardisierung, die nichts mit async/await zu tun haben. JavaScript-Engines konnten Performance-Optimierungen vornehmen, die Anwendungen mit intensiver Promise-Nutzung zugutekommen; ohne Standard wäre das nicht möglich gewesen
    • React steht nicht auf der Liste, weil signals im Gegensatz zu Preact kein Teil der React-Core-API sind
      Mein vages Bauchgefühl ist, dass signals einem verallgemeinerten useEffect() zu ähnlich sind und, wenn sie in React kämen, noch mehr Verwirrung darüber stiften würden, was während des Render-Zyklus passiert. Ob gut oder schlecht: React hat einen anderen Update-Ansatz als signals gewählt. Allerdings könnte ich mich bei der Anwendbarkeit irren
    • React ist nicht auf dieser Liste, weil seine Effekte deklarativ und nicht imperativ sind. Änderungen an props und Re-Rendering kann man ebenfalls als deklarativ auf einer Abstraktionsebene darüber sehen. useEffect kapselt imperatives Verhalten sauber
      Das sieht sehr nach Ember Data Binding aus und könnte am Ende zu einem imperativen Albtraum werden. Der Grundzustand ist eher eine „Schusswaffe auf den eigenen Fuß“, und um zu verhindern, dass es dazu kommt, braucht es eine enorme kognitive Last und Meta-Patterns
    • Vielleicht sollte man das einfach „EventEmitter“ nennen
      https://nodejs.org/en/learn/asynchronous-work/the-nodejs-eve...
  • Ich habe das Beispiel im verlinkten README nicht verstanden.
    // A library or framework defines effects based on other Signal primitives
    declare function effect(cb: () => void): (() => void);
    Welche Library? Welches Framework? An der Stelle bin ich ausgestiegen. Was ist effect?
    effect(() => element.innerText = parity.get());
    Woher weiß effect, dass es dieses Lambda jedes Mal aufrufen soll, wenn sich parity ändert? Ruft es dieses Lambda jedes Mal auf, wenn sich ein Signal ändert? Wenn ja, warum redet man dann über Caching? Vermutlich ist es das nicht.
    Wenn ich jedenfalls richtig verstanden habe, was die Autoren vermitteln wollten, wirkt die Signal-Idee an sich plausibel. Das große Problem bei so einer entkoppelten Architektur ist allerdings, dass man sich, sobald die Anwendung komplex genug wird, beim Nachverfolgen, warum ein bestimmtes Event ausgelöst wird, leicht verirrt. Idealerweise sollten Signals den Stacktrace so anpassen, dass beim Aufruf eines Callbacks bereits der Stacktrace des Codes enthalten ist, der das Signal ursprünglich getriggert hat.

    • Es gibt mehrere Libraries, die eine Funktion namens effect exportieren und es ermöglichen, beliebigen Code als Reaktion auf Signal-Updates auszuführen. Die Einführung zu Signals und Effects in der Preact-Dokumentation ist gut: https://preactjs.com/guide/v10/signals#effectfn
      Nach meinem Verständnis führt eine solche effect-Funktion den Callback zunächst einmal aus, um festzustellen, auf welche Signals während der Ausführung zugegriffen wurde, und ruft den Callback danach jedes Mal erneut auf, wenn ein Signal aktualisiert wird, von dem dieser Callback abhängt. Wenn der Zugriff auf Signals synchron und Single-Threaded ist, reicht die Tatsache, dass während der Ausführung des Callbacks auf ein Signal zugegriffen wurde, um zu wissen, dass der Callback dieses Signal abonnieren sollte.
      Das geht auch mit Gettern. Die effect-Funktion verfolgt dabei, auf welche Eigenschaft eines Signals in der Getter-Methode zugegriffen wurde; soweit ich weiß, hat Vue 2 früher diesen Ansatz verwendet. Man kann auch Objektzugriffe mit Proxys verfolgen. Das Beispiel im Vorschlag hat eine get-Methode, die aufgerufen wird, um auf den Signalwert zuzugreifen, und über die Ausführung dieser Methode lassen sich Abhängigkeiten verfolgen.
      [1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
      [2] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
    • Der Aufruf von parity.get() registriert eine Abhängigkeit für die an effect() übergebene Funktion. Wenn parity aktualisiert wird, wird diese Funktion aufgerufen.
      Sie wird nicht bei jeder Änderung irgendeines Signals aufgerufen, sondern nur, wenn sich ein Signal ändert, von dem sie abhängt.
      In diesem Fall hängt parity von isEven ab, und isEven hängt von counter ab. Wenn also counter aktualisiert wird, wird die gesamte Abhängigkeitskette invalidiert, parity wird invalidiert, und der Callback wird erneut ausgeführt.
    • Signal-Implementierungen bauen, egal wie sie heißen, im Allgemeinen einen dynamischen Abhängigkeitsgraphen auf, bei dem beim Lesen eines Knotens Kanten entstehen. In einem Tracking-Kontext wie diesem hypothetischen effect erzeugt ein Lesezugriff eine Kante zwischen dem Zustandsknoten des Signals und dem Berechnungsknoten des Effects; praktisch abonniert Letzterer damit spätere Schreibzugriffe auf Ersteren, um zu entscheiden, wann die Berechnung erneut ausgeführt werden soll.
    • effect ist eine beliebige Funktion, die man aufrufen möchte.
      Bei Signals weiß der Mechanismus zur Abhängigkeitsverfolgung, welche Werte neu berechnet werden müssen, und dadurch weiß das System auch, welche Funktionen erneut aufgerufen werden müssen.
    • Ein Watcher dürfte für die Implementierung von effect nötig sein.
  • Verwandt damit gibt es S.js: https://github.com/adamhaile/s
    Ich mag Signals. Beim Bau von UIs bevorzuge ich sie gegenüber praktisch allen anderen Grundbausteinen; vielleicht ist nur der Cassowary-Constraint-Algorithmus eine Ausnahme. In jeder Sprache, mit der ich zum Spaß arbeite, versuche ich, Signals nachzubauen.
    Aber ich sehe überhaupt nicht, dass das in die JavaScript-Sprache selbst gehört. Ich wünschte, man würde die Sprache eine Weile in Ruhe lassen. Die Leute haben schon jetzt Mühe, Schritt zu halten, und TC-39 schreckt bereits Leute von der Sprache ab.

  • Das sieht MobX, meinem liebsten JS-Effect-System, sehr ähnlich.
    Die MobX-Version sieht so aus:
    import { observable, computed, autorun } from 'mobx';
    const counter = observable.box(0);
    const isEven = computed(() => (counter.get() & 1) === 0);
    const parity = computed(() => isEven.get() ? "even" : "odd");
    autorun(() => { element.innerText = parity.get(); });
    setInterval(() => counter.set(counter.get() + 1), 1000);

    • MobX ist tatsächlich Signals. Allerdings sind es Signals, die Abhängigkeiten nicht explizit über Getter verfolgen, sondern implizit über Proxy-Objekte.
  • Das fühlt sich an wie: „Lasst uns das Framework, das ich gerade benutze, in die Standardbibliothek einbacken!“
    Ein bisschen so, als würde man sich den Namen der Freundin tätowieren lassen.

    • Darum geht es hier nicht.
      Es geht darum, einen Baustein, auf dessen Nutzung sich die meisten Frameworks hinentwickelt haben, in die Standardbibliothek aufzunehmen.
      Promises wurden ebenfalls in die Standardbibliothek aufgenommen, nachdem sie weit verbreitet waren; das hier ist ähnlich.