Vorschlag zur Einführung von Signals in JavaScript
(github.com/proposal-signals)- 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.ComputedundSignal.subtle.Watchersind 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
Promisein 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,parityundrenderdirekt miteinander verflochten, wodurch folgende Probleme entstehen- Zustand und Rendering-System sind stark gekoppelt
- Auch wenn sich
paritynicht ändert, etwa wenncountervon 2 auf 4 wechselt, entstehen unnötige Berechnungen und Renderings - Wenn andere UI-Teile nur Teile von
counter,isEvenoderparityabonnieren 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.StateundSignal.Computedauf 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 mitget(): TdefiniertSignal.State<T>ist ein beschreibbares Signal- Der Konstruktor nimmt einen Initialwert und Optionen entgegen
- Werte werden mit
get()gelesen und mitset(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.subtleenthält fortgeschrittene APIs, die eher für Framework-Autoren oder DevTools-Implementierungen gedacht sinduntrack(cb)erlaubt das Lesen von Signals ohne TrackingcurrentComputed()gibt das aktuell getrackte computed Signal zurückintrospectSources,introspectSinks,hasSinksundhasSourcessind APIs zur Beobachtung des GraphenWatcherist die Grundlage, um Änderungen an Signals zu erkennen und Effects sowie Scheduling auf Framework-Ebene zu implementieren
SignalOptions<T>unterstützt eine benutzerdefinierte Vergleichsfunktionequalssowiewatched- /unwatched-Hooks
Funktionsweise und Ausführungsmodell
- Ein Signal repräsentiert eine Datenzelle, die sich über die Zeit ändern kann, und ist entweder
stateodercomputed - 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
- Wird nach
- Der
notify-Callback eines Watchers kann während.set()synchron ausgeführt werden- Während
notifydürfen jedoch keine Signals gelesen oder geschrieben werden - Tatsächliche Lese- und Schreiboperationen müssen anschließend geplant werden
- Während
- 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,
untrackund 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
notifyeines Watchers verboten untrackwird 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.Watchereine Low-Level-Grundlage für die Implementierung von Effects- Wenn sich Abhängigkeiten eines beobachteten Signals ändern, wird
notifyaufgerufen - Mit
getPending()lassen sich noch dirty Signals prüfen - Effects, die ein Dispose benötigen, müssen mit
unwatchbereinigt werden
- Wenn sich Abhängigkeiten eines beobachteten Signals ändern, wird
- 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.unwatchnö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
- Das Framework verwaltet Watcher,
- 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
Proxyergä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
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.
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.
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
.valuezugreift.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
.valueinnerhalb der Komponente zugegriffen wurde.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.
Als Promises zu JavaScript hinzugefügt wurden, hatte ich Vorbehalte, weil ich befürchtete, überall
new Promiseschreiben zu müssen.In der Praxis kann ich die Male, in denen ich
new Promisedirekt 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.
new Promiseschreiben.Das frühe
.thenvon 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
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
Ü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
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?
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
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
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 CodeEs ist auch schwer zu garantieren, dass nicht irgendein Listener eine solche Kette von Triggern erzeugt
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
innerTextwird „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 wirklichWenn es komplexer wird, ist es nicht mehr leicht
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
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
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/promisesselectausführen könnte, hätte das syntaktische Vorteile gegenüber der Verwaltung mehrerer gleichzeitiger Messaging-Mechanismen mit Callbacks. Zum Beispiel indem man signals inPromise.anyeinbeziehen kannx instanceof Promisefunktioniert einfach nicht. Wenn diethen-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 wirdfinallyausgefü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
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 irrenuseEffectkapselt imperatives Verhalten sauberDas 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
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 primitivesdeclare 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 sichparityä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.
effectexportieren 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#effectfnNach 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 eineget-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...
parity.get()registriert eine Abhängigkeit für die aneffect()übergebene Funktion. Wennparityaktualisiert 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
parityvonisEvenab, undisEvenhängt voncounterab. Wenn alsocounteraktualisiert wird, wird die gesamte Abhängigkeitskette invalidiert,paritywird invalidiert, und der Callback wird erneut ausgeführt.effecterzeugt 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.effectist 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.
effectnö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);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.
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.