Stufe 1 (Erklärung)
Verfechter des TC39-Vorschlags: Daniel Ehrenberg, Yehuda Katz, Jatin Ramanathan, Shay Lewis, Kristen Hewell Garrett, Dominic Gannaway, Preston Sego, Milo M, Rob Eisenberg
Originalautoren: Rob Eisenberg und Daniel Ehrenberg
Dieses Dokument beschreibt eine frühe gemeinsame Richtung für Signale in JavaScript, ähnlich dem Promises/A+-Ansatz, der den von TC39 in ES2015 standardisierten Promises vorausging. Probieren Sie es selbst aus, indem Sie eine Polyfüllung verwenden.
Ähnlich wie bei Promises/A+ liegt der Schwerpunkt dieser Bemühungen auf der Angleichung des JavaScript-Ökosystems. Wenn diese Ausrichtung gelingt, könnte auf der Grundlage dieser Erfahrung ein Standard entstehen. Mehrere Framework-Autoren arbeiten hier an einem gemeinsamen Modell, das ihren Reaktivitätskern unterstützen könnte. Der aktuelle Entwurf basiert auf Design-Input der Autoren/Betreuer von Angular, Bubble, Ember, FAST, MobX, Preact, Qwik, RxJS, Solid, Starbeam, Svelte, Vue, Wiz und mehr …
Anders als bei Promises/A+ versuchen wir nicht, eine Lösung für eine gemeinsame, entwicklerorientierte Oberflächen-API zu finden, sondern vielmehr die genaue Kernsemantik des zugrunde liegenden Signaldiagramms. Dieser Vorschlag enthält zwar eine vollständig konkrete API, die API richtet sich jedoch nicht an die meisten Anwendungsentwickler. Stattdessen eignet sich die Signal-API hier besser für darauf aufbauende Frameworks und bietet Interoperabilität durch einen gemeinsamen Signalgraphen und Auto-Tracking-Mechanismus.
Der Plan für diesen Vorschlag besteht darin, ein umfangreiches frühes Prototyping durchzuführen, einschließlich der Integration in mehrere Frameworks, bevor wir über Stufe 1 hinausgehen. Wir sind nur dann an der Standardisierung von Signalen interessiert, wenn sie für den Einsatz in der Praxis in mehreren Frameworks geeignet sind und echte Vorteile gegenüber Frameworks bieten. bereitgestellte Signale. Wir hoffen, dass wir durch eine umfangreiche frühe Prototypenentwicklung diese Informationen erhalten. Weitere Einzelheiten finden Sie weiter unten im Abschnitt „Status und Entwicklungsplan“.
Um eine komplizierte Benutzeroberfläche (UI) zu entwickeln, müssen JavaScript-Anwendungsentwickler den Status auf effiziente Weise speichern, berechnen, ungültig machen, synchronisieren und an die Ansichtsebene der Anwendung übertragen. Bei Benutzeroberflächen geht es in der Regel um mehr als nur die Verwaltung einfacher Werte, häufig geht es aber auch um die Darstellung berechneter Zustände, die von einem komplexen Baum anderer Werte oder Zuständen abhängig sind, die ebenfalls selbst berechnet werden. Das Ziel von Signals besteht darin, eine Infrastruktur für die Verwaltung dieses Anwendungsstatus bereitzustellen, damit sich Entwickler auf die Geschäftslogik statt auf diese sich wiederholenden Details konzentrieren können.
Signalähnliche Konstrukte haben sich unabhängig voneinander auch in Nicht-UI-Kontexten als nützlich erwiesen, insbesondere in Build-Systemen, um unnötige Neuerstellungen zu vermeiden.
Signale werden bei der reaktiven Programmierung verwendet, um die Notwendigkeit zu beseitigen, Aktualisierungen in Anwendungen zu verwalten.
Ein deklaratives Programmiermodell zur Aktualisierung basierend auf Zustandsänderungen.
aus „Was ist Reaktivität?“ .
Bei einer gegebenen Variablen, counter
, möchten Sie im DOM rendern, ob der Zähler gerade oder ungerade ist. Immer wenn sich der counter
ändert, möchten Sie das DOM mit der neuesten Parität aktualisieren. In Vanilla JS könnten Sie so etwas haben:
let counter = 0;const setCounter = (value) => { Zähler = Wert; render();};const isEven = () => (counter & 1) == 0;const parity = () => isEven() ? "even" : "odd";const render = () => element.innerText = parity();// Externe Aktualisierungen des Zählers simulieren...setInterval(() => setCounter(counter + 1), 1000);
Das bringt eine Reihe von Problemen mit sich...
Der counter
ist laut und kesselplattenlastig.
Der counter
ist eng an das Rendering-System gekoppelt.
Wenn sich der counter
ändert, parity
jedoch nicht (z. B. der Zähler geht von 2 auf 4), führen wir unnötige Berechnungen der Parität und unnötiges Rendern durch.
Was ist, wenn ein anderer Teil unserer Benutzeroberfläche nur gerendert werden möchte, wenn der counter
aktualisiert wird?
Was ist, wenn ein anderer Teil unserer Benutzeroberfläche nur von isEven
oder parity
abhängig ist?
Selbst in diesem relativ einfachen Szenario treten schnell eine Reihe von Problemen auf. Wir könnten versuchen, diese Probleme zu umgehen, indem wir Pub/Sub für den counter
einführen. Dies würde es zusätzlichen Verbrauchern des counter
ermöglichen, ihre eigenen Reaktionen auf Zustandsänderungen hinzuzufügen.
Allerdings stecken uns immer noch die folgenden Probleme fest:
Die Renderfunktion, die nur von parity
abhängig ist, muss stattdessen „wissen“, dass sie tatsächlich counter
abonnieren muss.
Es ist nicht möglich, die Benutzeroberfläche allein basierend auf isEven
oder parity
zu aktualisieren, ohne direkt mit counter
zu interagieren.
Wir haben unseren Boilerplate erhöht. Wann immer Sie etwas verwenden, geht es nicht nur darum, eine Funktion aufzurufen oder eine Variable zu lesen, sondern auch darum, dort ein Abonnement abzuschließen und Aktualisierungen vorzunehmen. Besonders kompliziert ist auch die Verwaltung der Abmeldung.
Jetzt könnten wir ein paar Probleme lösen, indem wir pub/sub nicht nur zum counter
sondern auch zu isEven
und parity
hinzufügen. Wir müssten dann isEven
für counter
abonnieren, parity
für isEven
und render
für parity
. Unglücklicherweise ist nicht nur unser Boilerplate-Code explodiert, sondern wir müssen uns auch mit einer Unmenge an Abonnementbuchhaltung befassen und es könnte zu einer Speicherleckkatastrophe kommen, wenn wir nicht alles auf die richtige Art und Weise bereinigen. Wir haben also einige Probleme gelöst, aber eine ganz neue Kategorie von Problemen und eine Menge Code geschaffen. Erschwerend kommt hinzu, dass wir diesen gesamten Prozess für jeden Staat in unserem System durchlaufen müssen.
Datenbindungsabstraktionen in UIs für das Modell und die Ansicht sind seit langem ein zentraler Bestandteil von UI-Frameworks in mehreren Programmiersprachen, obwohl in JS oder der Webplattform kein solcher Mechanismus integriert ist. Innerhalb von JS-Frameworks und -Bibliotheken wurde viel mit verschiedenen Möglichkeiten zur Darstellung dieser Bindung experimentiert, und die Erfahrung hat die Leistungsfähigkeit eines unidirektionalen Datenflusses in Verbindung mit einem erstklassigen Datentyp gezeigt, der eine Zustands- oder Berechnungszelle darstellt abgeleitet von anderen Daten, heute oft als „Signale“ bezeichnet. Dieser erstklassige Reactive-Value-Ansatz scheint 2010 mit Knockout seinen ersten populären Auftritt in Open-Source-JavaScript-Webframeworks gehabt zu haben. In den Jahren seitdem sind viele Variationen und Implementierungen entstanden. In den letzten drei bis vier Jahren haben das Signal-Primitiv und verwandte Ansätze weiter an Bedeutung gewonnen, und nahezu jede moderne JavaScript-Bibliothek oder jedes moderne JavaScript-Framework verfügt unter dem einen oder anderen Namen über etwas Ähnliches.
Um Signale zu verstehen, werfen wir einen Blick auf das obige Beispiel, neu interpretiert mit einer Signal-API, die weiter unten näher erläutert wird.
const counter = new Signal.State(0);const isEven = new Signal.Computed(() => (counter.get() & 1) == 0);const parity = new Signal.Computed(() => isEven .get() ? "even" : "odd");// Eine Bibliothek oder ein Framework definiert Effekte basierend auf anderen Signalprimitivendeclare function effect(cb: () => void): (() => void);effect(( ) => element.innerText = parity.get());// Externe Aktualisierungen des Zählers simulieren...setInterval(() => counter.set(counter.get() + 1), 1000);
Es gibt ein paar Dinge, die wir sofort sehen können:
Wir haben das verrauschte Boilerplate rund um die counter
aus unserem vorherigen Beispiel eliminiert.
Es gibt eine einheitliche API zur Verarbeitung von Werten, Berechnungen und Nebenwirkungen.
Es gibt kein Zirkelverweisproblem oder umgekehrte Abhängigkeiten zwischen counter
und render
.
Es gibt keine manuellen Abonnements und auch keine Buchhaltung ist erforderlich.
Es gibt eine Möglichkeit, das Timing/die Planung von Nebenwirkungen zu steuern.
Signale geben uns jedoch viel mehr als das, was auf der Oberfläche der API zu sehen ist:
Automatische Abhängigkeitsverfolgung – Ein berechnetes Signal erkennt automatisch alle anderen Signale, von denen es abhängig ist, unabhängig davon, ob es sich bei diesen Signalen um einfache Werte oder andere Berechnungen handelt.
Lazy Evaluation – Berechnungen werden nicht sofort ausgewertet, wenn sie deklariert werden, und sie werden auch nicht sofort ausgewertet, wenn sich ihre Abhängigkeiten ändern. Sie werden nur ausgewertet, wenn ihr Wert explizit angefordert wird.
Memoisierung – Berechnete Signale speichern ihren letzten Wert zwischen, sodass Berechnungen, deren Abhängigkeiten sich nicht ändern, nicht erneut ausgewertet werden müssen, unabhängig davon, wie oft auf sie zugegriffen wird.
Jede Signalimplementierung verfügt über einen eigenen Auto-Tracking-Mechanismus, um die bei der Auswertung eines berechneten Signals gefundenen Quellen zu verfolgen. Dies erschwert die gemeinsame Nutzung von Modellen, Komponenten und Bibliotheken zwischen verschiedenen Frameworks – sie weisen tendenziell eine falsche Kopplung mit ihrer View-Engine auf (da Signale normalerweise als Teil von JS-Frameworks implementiert werden).
Ein Ziel dieses Vorschlags besteht darin, das reaktive Modell vollständig von der Rendering-Ansicht zu entkoppeln, sodass Entwickler auf neue Rendering-Technologien migrieren können, ohne ihren Nicht-UI-Code neu zu schreiben, oder gemeinsame reaktive Modelle in JS entwickeln können, die in verschiedenen Kontexten bereitgestellt werden. Leider hat es sich aufgrund der Versionierung und Duplizierung als unpraktisch herausgestellt, über Bibliotheken auf JS-Ebene ein starkes Maß an Freigabe zu erreichen – integrierte Funktionen bieten eine stärkere Freigabegarantie.
Da häufig verwendete Bibliotheken integriert sind, stellt die Auslieferung von weniger Code immer einen kleinen potenziellen Leistungsschub dar, aber Implementierungen von Signalen sind im Allgemeinen recht klein, sodass wir nicht erwarten, dass dieser Effekt sehr groß sein wird.
Wir vermuten, dass native C++-Implementierungen signalbezogener Datenstrukturen und Algorithmen um einen konstanten Faktor etwas effizienter sein können als das, was in JS erreichbar ist. Es sind jedoch keine algorithmischen Änderungen im Vergleich zu dem zu erwarten, was in einer Polyfüllung vorhanden wäre. Es wird nicht erwartet, dass Engines hier Zauberei sind, und die Reaktivitätsalgorithmen selbst werden wohldefiniert und eindeutig sein.
Die Champion-Gruppe beabsichtigt, verschiedene Implementierungen von Signalen zu entwickeln und diese zur Untersuchung dieser Leistungsmöglichkeiten zu nutzen.
Bei vorhandenen Signalbibliotheken in der JS-Sprache kann es schwierig sein, Dinge zu verfolgen wie:
Der Aufrufstapel über eine Kette berechneter Signale, der die Kausalkette für einen Fehler zeigt
Das Referenzdiagramm zwischen Signalen, wenn eines von einem anderen abhängt – wichtig beim Debuggen der Speichernutzung
Integrierte Signale ermöglichen JS-Laufzeiten und DevTools möglicherweise eine verbesserte Unterstützung für die Überprüfung von Signalen, insbesondere für das Debuggen oder die Leistungsanalyse, unabhängig davon, ob dies in Browser integriert ist oder über eine gemeinsame Erweiterung erfolgt. Bestehende Tools wie der Elementinspektor, der Leistungs-Snapshot und Speicherprofiler könnten aktualisiert werden, um Signale in ihrer Informationsdarstellung gezielt hervorzuheben.
Im Allgemeinen verfügte JavaScript über eine relativ minimale Standardbibliothek, aber in TC39 ging der Trend dahin, JS eher zu einer „batteriebetriebenen“ Sprache zu machen, die über eine hochwertige, integrierte Funktionalität verfügt. Temporal ersetzt beispielsweise moment.js und eine Reihe kleiner Funktionen, z. B. Array.prototype.flat
und Object.groupBy
ersetzen viele Lodash-Anwendungsfälle. Zu den Vorteilen gehören kleinere Paketgrößen, verbesserte Stabilität und Qualität, weniger Lernaufwand beim Beitritt zu einem neuen Projekt und ein allgemein gemeinsames Vokabular für alle JS-Entwickler.
Aktuelle Arbeiten im W3C und von Browser-Implementierern zielen darauf ab, native Vorlagen in HTML zu integrieren (DOM Parts and Template Instantiation). Darüber hinaus untersucht die W3C Web Components CG die Möglichkeit, Web Components zu erweitern, um eine vollständig deklarative HTML-API anzubieten. Um diese beiden Ziele zu erreichen, benötigt HTML letztendlich ein reaktives Grundelement. Darüber hinaus sind viele ergonomische Verbesserungen des DOM durch die Integration von Signalen vorstellbar und wurden von der Community gefordert.
Beachten Sie, dass diese Integration ein separater, später erfolgender Versuch wäre und nicht Teil dieses Vorschlags selbst.
Standardisierungsbemühungen können manchmal gerade auf „Community“-Ebene hilfreich sein, auch ohne Änderungen an den Browsern. Die Bemühungen von Signals bringen viele verschiedene Framework-Autoren zu einer intensiven Diskussion über die Natur von Reaktivität, Algorithmen und Interoperabilität zusammen. Dies hat sich bereits als nützlich erwiesen und rechtfertigt nicht die Aufnahme in JS-Engines und Browser; Signale sollten nur dann zum JavaScript-Standard hinzugefügt werden, wenn es über den ermöglichten Ökosystem-Informationsaustausch hinaus erhebliche Vorteile gibt.
Es stellt sich heraus, dass sich bestehende Signalbibliotheken im Kern nicht allzu sehr voneinander unterscheiden. Dieser Vorschlag zielt darauf ab, auf ihrem Erfolg aufzubauen, indem die wichtigen Qualitäten vieler dieser Bibliotheken umgesetzt werden.
Ein Signaltyp, der den Zustand darstellt, also ein beschreibbares Signal. Dies ist ein Wert, den andere lesen können.
Ein berechneter/memo/abgeleiteter Signaltyp, der von anderen abhängt und träge berechnet und zwischengespeichert wird.
Die Berechnung erfolgt verzögert, das heißt, berechnete Signale werden standardmäßig nicht erneut berechnet, wenn sich eine ihrer Abhängigkeiten ändert, sondern nur ausgeführt, wenn jemand sie tatsächlich liest.
Die Berechnung erfolgt „störungsfrei“, d. h. es werden nie unnötige Berechnungen durchgeführt. Dies bedeutet, dass, wenn eine Anwendung ein berechnetes Signal liest, eine topologische Sortierung der potenziell fehlerhaften Teile des Diagramms erfolgt, um etwaige Duplikate zu beseitigen.
Die Berechnung wird zwischengespeichert, was bedeutet, dass das berechnete Signal beim Zugriff nicht neu berechnet wird, wenn sich nach der letzten Änderung einer Abhängigkeit keine Abhängigkeiten geändert haben.
Benutzerdefinierte Vergleiche sind sowohl für berechnete Signale als auch für Zustandssignale möglich, um festzustellen, wann weitere berechnete Signale, die davon abhängen, aktualisiert werden sollten.
Reaktionen auf die Bedingung, dass ein berechnetes Signal eine seiner Abhängigkeiten (oder verschachtelten Abhängigkeiten) hat, werden „schmutzig“ und ändern sich, was bedeutet, dass der Wert des Signals möglicherweise veraltet ist.
Diese Reaktion soll dazu dienen, größere Arbeiten für einen späteren Zeitpunkt zu planen.
Effekte werden im Hinblick auf diese Reaktionen sowie die Planung auf Framework-Ebene implementiert.
Berechnete Signale müssen darauf reagieren können, ob sie als (verschachtelte) Abhängigkeit einer dieser Reaktionen registriert sind.
Ermöglichen Sie JS-Frameworks, ihre eigene Planung durchzuführen. Keine integrierte erzwungene Planung im Promise-Stil.
Synchrone Reaktionen sind erforderlich, um die Planung späterer Arbeiten basierend auf der Framework-Logik zu ermöglichen.
Schreibvorgänge sind synchron und werden sofort wirksam (ein Framework, das Schreibvorgänge stapelweise, kann dies zusätzlich tun).
Es ist möglich, die Prüfung, ob ein Effekt möglicherweise „verschmutzt“ ist, von der tatsächlichen Ausführung des Effekts zu trennen (was einen zweistufigen Effektplaner ermöglicht).
Fähigkeit, Signale zu lesen, ohne die Aufzeichnung von Abhängigkeiten auszulösen ( untrack
)
Ermöglichen Sie die Zusammensetzung verschiedener Codebasen, die Signale/Reaktivität verwenden, z. B.
Gemeinsame Verwendung mehrerer Frameworks, soweit es um Tracking/Reaktivität selbst geht (Modulo-Auslassungen, siehe unten)
Framework-unabhängige reaktive Datenstrukturen (z. B. rekursiv reaktiver Store-Proxy, reaktive Map und Set und Array usw.)
Entmutigen/verbieten Sie den naiven Missbrauch synchroner Reaktionen.
Zuverlässigkeitsrisiko: Bei unsachgemäßer Verwendung kann es zu „Störungen“ kommen: Wenn das Rendern sofort erfolgt, wenn ein Signal festgelegt wird, wird dem Endbenutzer möglicherweise ein unvollständiger Anwendungsstatus angezeigt. Daher sollte diese Funktion nur verwendet werden, um Arbeiten intelligent für später zu planen, sobald die Anwendungslogik abgeschlossen ist.
Lösung: Verbieten Sie das Lesen und Schreiben von Signalen innerhalb eines synchronen Reaktionsrückrufs
Entmutigen Sie untrack
und markieren Sie ihre fehlerhafte Natur
Soliditätsrisiko: Ermöglicht die Erstellung berechneter Signale, deren Wert von anderen Signalen abhängt, die jedoch nicht aktualisiert werden, wenn sich diese Signale ändern. Es sollte verwendet werden, wenn die nicht verfolgten Zugriffe das Ergebnis der Berechnung nicht ändern.
Lösung: Die API ist im Namen als „unsicher“ gekennzeichnet.
Hinweis: Dieser Vorschlag ermöglicht das Lesen und Schreiben von Signalen aus berechneten Signalen und Effektsignalen, ohne die auf Lesevorgänge folgenden Schreibvorgänge einzuschränken, trotz des Risikos der Solidität. Diese Entscheidung wurde getroffen, um Flexibilität und Kompatibilität bei der Integration mit Frameworks zu wahren.
Muss eine solide Basis für mehrere Frameworks sein, um ihre Signal-/Reaktivitätsmechanismen zu implementieren.
Sollte eine gute Basis für rekursive Store-Proxys, dekoratorbasierte Klassenfeldreaktivität und APIs im .value
und [state, setState]
-Stil sein.
Die Semantik ist in der Lage, die gültigen Muster auszudrücken, die von verschiedenen Frameworks ermöglicht werden. Beispielsweise sollte es möglich sein, dass diese Signale die Grundlage für entweder sofort reflektierte Schreibvorgänge oder für Schreibvorgänge sind, die gestapelt und später angewendet werden.
Es wäre schön, wenn diese API direkt von JavaScript-Entwicklern genutzt werden könnte.
Idee: Stellen Sie alle Hooks bereit, schließen Sie jedoch nach Möglichkeit Fehler bei Missbrauch ein.
Idee: Fügen Sie subtile APIs in einen subtle
Namespace ein, ähnlich wie crypto.subtle
, um die Grenze zwischen APIs zu markieren, die für eine fortgeschrittenere Verwendung wie die Implementierung eines Frameworks oder die Erstellung von Entwicklungstools erforderlich sind, und einer alltäglicheren Anwendungsentwicklungsverwendung wie der Instanziierung von Signalen zur Verwendung mit einem Rahmen.
Es ist jedoch wichtig, nicht genau die gleichen Namen zu verschleiern!
Wenn ein Feature mit einem Ökosystemkonzept übereinstimmt, ist die Verwendung eines gemeinsamen Vokabulars sinnvoll.
Spannung zwischen „Benutzerfreundlichkeit durch JS-Entwickler“ und „Bereitstellung aller Hooks für Frameworks“
Seien Sie bei guter Leistung umsetzbar und nutzbar – die Oberflächen-API verursacht nicht zu viel Overhead
Aktivieren Sie Unterklassen, damit Frameworks ihre eigenen Methoden und Felder, einschließlich privater Felder, hinzufügen können. Dies ist wichtig, um die Notwendigkeit zusätzlicher Zuweisungen auf Rahmenebene zu vermeiden. Siehe „Speicherverwaltung“ weiter unten.
Wenn möglich: Ein berechnetes Signal sollte für die Müllsammlung geeignet sein, wenn nichts Lebendiges darauf für mögliche zukünftige Lesevorgänge verweist, selbst wenn es mit einem umfassenderen Diagramm verknüpft ist, das am Leben bleibt (z. B. durch Lesen eines Zustands, der aktiv bleibt).
Beachten Sie, dass die meisten Frameworks heutzutage eine explizite Entsorgung berechneter Signale erfordern, wenn sie einen Verweis auf oder von einem anderen Signalgraphen haben, der aktiv bleibt.
Dies ist am Ende nicht so schlimm, wenn ihre Lebensdauer an die Lebensdauer einer UI-Komponente gebunden ist und Effekte ohnehin entsorgt werden müssen.
Wenn die Ausführung mit dieser Semantik zu teuer ist, sollten wir der API unten eine explizite Entsorgung (oder „Aufhebung der Verknüpfung“) von berechneten Signalen hinzufügen, die derzeit nicht vorhanden ist.
Ein separates damit verbundenes Ziel: Minimieren Sie die Anzahl der Zuweisungen, z. B.
um ein beschreibbares Signal zu erzeugen (vermeiden Sie zwei separate Abschlüsse + Array)
um Effekte umzusetzen (einen Abschluss für jede einzelne Reaktion vermeiden)
Vermeiden Sie in der API zum Beobachten von Signaländerungen die Erstellung zusätzlicher temporärer Datenstrukturen
Lösung: Klassenbasierte API, die die Wiederverwendung von Methoden und Feldern ermöglicht, die in Unterklassen definiert sind
Nachfolgend finden Sie eine erste Idee einer Signal-API. Beachten Sie, dass es sich hierbei lediglich um einen frühen Entwurf handelt und wir mit Änderungen im Laufe der Zeit rechnen. Beginnen wir mit den vollständigen .d.ts
, um eine Vorstellung von der Gesamtform zu bekommen, und besprechen dann die Details dessen, was das alles bedeutet.
Schnittstelle Signal<T> {// Holen Sie sich den Wert von signalget(): T;}namespace Signal {// Eine Lese-/Schreib-Signalklasse State<T> implementiert Signal<T> {// Erstellen Sie ein Statussignal, beginnend mit dem Wert tconstructor(t: T, options?: SignalOptions<T>);// Holen Sie sich den Wert des Signalsget(): T;// Setzen Sie den Statussignalwert auf tset(t: T): void;}// Ein Signal Das ist eine Formel, die auf anderen basiert Signalsclass Computed<T = unlimited> implementiert Signal<T> {// Erstellt ein Signal, das den vom Rückruf zurückgegebenen Wert ergibt.// Der Rückruf wird mit diesem Signal als this value.constructor(cb: (this: Computed< aufgerufen T>) => T, options?: SignalOptions<T>);// Holen Sie sich den Wert von signalget(): T;}// Dieser Namespace enthält „erweiterte“ Funktionen, die besser// den Framework-Autoren überlassen werden sollten als Anwendungsentwickler.// Analog zu „crypto.subtle“namespace subtil {// Führen Sie einen Rückruf mit deaktivierter Verfolgung aus. function untrack<T>(cb: () => T): T;// Holen Sie sich das aktuell berechnete Signal, das verfolgt wird Jedes Signal liest, falls vorhandenFunktion currentComputed(): Berechnet | null;// Gibt eine geordnete Liste aller Signale zurück, auf die dieses Signal während der letzten Auswertung // verwiesen hat.// Für einen Watcher wird die Menge der Signale aufgelistet, die er beobachtet.function introspectSources(s: Berechnet | Watcher): (Zustand | Berechnet)[];// Gibt die Beobachter zurück, in denen dieses Signal enthalten ist, plus alle// Berechneten Signale, die dieses Signal zum letzten Mal gelesen haben, als sie ausgewertet wurden,// wenn das berechnete Signal (rekursiv) ist watched.function introspectSinks(s: State | Computed): (Computed | Watcher)[];// True, wenn dieses Signal „live“ ist, indem es von einem Watcher beobachtet wird,// oder von einem berechneten Signal gelesen wird Das ist (rekursiv) live.function hasSinks(s: State | Computed): boolean;// True, wenn dieses Element „reaktiv“ ist, d. h. es hängt// von einem anderen Signal ab. Ein berechneter Wert, bei dem hasSources falsch ist//, gibt immer die gleiche Konstante zurück.function hasSources(s: Computed | Watcher): boolean;class Watcher {// Wenn in eine (rekursive) Quelle von Watcher geschrieben wird, rufen Sie diesen Rückruf auf,// wenn es nicht bereits seit dem letzten `watch`-Aufruf aufgerufen wurde.// Während des notify.constructor(notify: (this: Watcher) => dürfen keine Signale gelesen oder geschrieben werden void);// Fügen Sie diese Signale zum Satz des Watchers hinzu und stellen Sie den Watcher so ein, dass er seinen// Benachrichtigungsrückruf ausführt, wenn sich das nächste Mal ein Signal im Satz (oder einer seiner Abhängigkeiten) ändert.// Kann ohne Argumente aufgerufen werden, nur um Setzen Sie den Status „benachrichtigt“ zurück, sodass// der Benachrichtigungsrückruf erneut aufgerufen wird.watch(...s: Signal[]): void;// Diese Signale aus dem überwachten Satz entfernen (z. B. für einen Effekt, der ist disponiert)unwatch(...s: Signal[]): void;// Gibt die Menge der Quellen im Satz des Watchers zurück, die noch verschmutzt sind, oder ist ein berechnetes Signal// mit einer Quelle, die verschmutzt oder ausstehend ist und noch nicht verschmutzt ist. wurde noch nicht erneut ausgewertetgetPending(): Signal[];}// Hooks, um zu beobachten, ob sie beobachtet werden oder nicht mehr beobachtet werdenvar beobachtet: Symbol;var unwatched: Symbol;}interface SignalOptions<T> {// Benutzerdefinierte Vergleichsfunktion zwischen alten und neuer Wert. Standard: Object.is.// Das Signal wird als dieser Wert für context.equals?: (this: Signal<T>, t: T, t2: T) => boolean;// Callback aufgerufen, wenn isWatched wird true, wenn es vorher false war[Signal.subtle.watched]?: (this: Signal<T>) => void;// Callback wird immer dann aufgerufen, wenn isWatched false wird, wenn es vorher war true[Signal.subtle.unwatched]?: (this: Signal<T>) => void;}}
Ein Signal stellt eine Datenzelle dar, die sich im Laufe der Zeit ändern kann. Signale können entweder „Zustand“ (nur ein manuell eingestellter Wert) oder „berechnet“ (eine auf anderen Signalen basierende Formel) sein.
Berechnete Signale funktionieren, indem sie automatisch verfolgen, welche anderen Signale während ihrer Auswertung gelesen werden. Wenn ein berechneter Wert gelesen wird, prüft er, ob sich zuvor aufgezeichnete Abhängigkeiten geändert haben, und wertet sich gegebenenfalls neu aus. Wenn mehrere berechnete Signale verschachtelt sind, geht die gesamte Verfolgungszuordnung an das innerste Signal.
Berechnete Signale sind lazy, also pull-basiert: Sie werden nur dann neu ausgewertet, wenn auf sie zugegriffen wird, selbst wenn sich eine ihrer Abhängigkeiten zuvor geändert hat.
Der an berechnete Signale übergebene Rückruf sollte im Allgemeinen „rein“ im Sinne einer deterministischen, nebenwirkungsfreien Funktion der anderen Signale sein, auf die er zugreift. Gleichzeitig ist der Zeitpunkt des Aufrufs des Rückrufs deterministisch, sodass Nebenwirkungen mit Bedacht eingesetzt werden können.
Signale verfügen über eine ausgeprägte Caching-/Speicherfunktion: Sowohl Zustands- als auch berechnete Signale merken sich ihren aktuellen Wert und lösen nur dann eine Neuberechnung der berechneten Signale aus, die auf sie verweisen, wenn sie sich tatsächlich ändern. Ein wiederholter Vergleich alter und neuer Werte ist nicht einmal erforderlich – der Vergleich wird einmal durchgeführt, wenn das Quellsignal zurückgesetzt/neu bewertet wird, und der Signalmechanismus verfolgt, welche Dinge, die auf dieses Signal verweisen, nicht basierend auf dem neuen aktualisiert wurden Wert noch. Intern wird dies im Allgemeinen durch „Diagrammfärbung“ dargestellt, wie in (Milos Blogbeitrag) beschrieben.
Berechnete Signale verfolgen ihre Abhängigkeiten dynamisch – jedes Mal, wenn sie ausgeführt werden, hängen sie möglicherweise von anderen Dingen ab, und dieser genaue Abhängigkeitssatz wird im Signaldiagramm aktuell gehalten. Das heißt, wenn Sie eine Abhängigkeit haben, die nur von einem Zweig benötigt wird und die vorherige Berechnung den anderen Zweig berücksichtigt hat, führt eine Änderung an diesem vorübergehend nicht verwendeten Wert nicht dazu, dass das berechnete Signal neu berechnet wird, selbst wenn es gezogen wird.
Im Gegensatz zu JavaScript Promises läuft in Signals alles synchron:
Das Setzen eines Signals auf einen neuen Wert ist synchron, und dies spiegelt sich sofort wider, wenn anschließend ein berechnetes Signal gelesen wird, das davon abhängt. Für diese Mutation gibt es keine integrierte Stapelverarbeitung.
Das Lesen berechneter Signale erfolgt synchron – ihr Wert ist immer verfügbar.
Der notify
in Watchers wird, wie unten erläutert, synchron ausgeführt, während des .set()
-Aufrufs, der ihn ausgelöst hat (jedoch nachdem die Diagrammfärbung abgeschlossen ist).
Wie Promises können Signale einen Fehlerzustand darstellen: Wenn der Callback eines berechneten Signals einen Fehler auslöst, wird dieser Fehler genau wie ein anderer Wert zwischengespeichert und jedes Mal erneut ausgelöst, wenn das Signal gelesen wird.
Eine Signal
stellt die Fähigkeit dar, einen sich dynamisch ändernden Wert zu lesen, dessen Aktualisierungen im Laufe der Zeit verfolgt werden. Es umfasst implizit auch die Möglichkeit, das Signal zu abonnieren, implizit über einen verfolgten Zugriff von einem anderen berechneten Signal aus.
Die API ist hier so konzipiert, dass sie dem sehr groben Ökosystemkonsens eines großen Teils der Signalbibliotheken bei der Verwendung von Namen wie „Signal“, „berechnet“ und „Zustand“ entspricht. Der Zugriff auf berechnete und Zustandssignale erfolgt jedoch über eine .get()
Methode, was im Widerspruch zu allen gängigen Signal-APIs steht, die entweder einen Accessor .value
-Stil oder eine signal()
-Aufrufsyntax verwenden.
Die API ist darauf ausgelegt, die Anzahl der Zuweisungen zu reduzieren, um Signale für die Einbettung in JavaScript-Frameworks geeignet zu machen und gleichzeitig die gleiche oder eine bessere Leistung als bestehende, an das Framework angepasste Signale zu erreichen. Dies impliziert:
Statussignale sind ein einzelnes beschreibbares Objekt, auf das über dieselbe Referenz sowohl zugegriffen als auch festgelegt werden kann. (Siehe Auswirkungen weiter unten im Abschnitt „Fähigkeitstrennung“.)
Sowohl Zustands- als auch berechnete Signale sind so konzipiert, dass sie in Unterklassen unterteilt werden können, um die Fähigkeit von Frameworks zu erleichtern, zusätzliche Eigenschaften über öffentliche und private Klassenfelder (sowie Methoden zur Verwendung dieses Zustands) hinzuzufügen.
Verschiedene Rückrufe (z. B. equals
, der berechnete Rückruf) werden mit dem relevanten Signal als this
Wert für den Kontext aufgerufen, sodass kein neuer Abschluss pro Signal erforderlich ist. Stattdessen kann der Kontext in zusätzlichen Eigenschaften des Signals selbst gespeichert werden.
Einige von dieser API erzwungene Fehlerbedingungen:
Es ist ein Fehler, einen berechneten Wert rekursiv zu lesen.
Der notify
Callback eines Watchers kann keine Signale lesen oder schreiben
Wenn der Rückruf eines berechneten Signals einen Fehler auslöst, lösen nachfolgende Zugriffe auf das Signal diesen zwischengespeicherten Fehler erneut aus, bis sich eine der Abhängigkeiten ändert und der Fehler neu berechnet wird.
Einige Bedingungen, die nicht durchgesetzt werden:
Berechnete Signale können synchron innerhalb ihres Rückrufs in andere Signale schreiben
Arbeit, die durch den notify
eines Beobachters in die Warteschlange gestellt wird, kann Signale lesen oder schreiben, wodurch es möglich wird, klassische React-Antimuster in Bezug auf Signale zu replizieren!
Die oben definierte Watcher
-Schnittstelle bildet die Grundlage für die Implementierung typischer JS-APIs für Effekte: Rückrufe, die ausschließlich wegen ihres Nebeneffekts erneut ausgeführt werden, wenn sich andere Signale ändern. Die oben im ersten Beispiel verwendete effect
kann wie folgt definiert werden:
// Diese Funktion befindet sich normalerweise in einer Bibliothek/einem Framework und nicht in Anwendungscode. // HINWEIS: Diese Planungslogik ist zu einfach, um nützlich zu sein. Nicht kopieren/einfügen.let pending = false;let w = new Signal.subtle.Watcher(() => {if (!pending) {pending = true;queueMicrotask(() => {pending = false;for (let s of w.getPending()) s.get();w.watch();});}});// Ein Effekteffektsignal, das zu cb ausgewertet wird, das einen Lesevorgang von// selbst in der Mikrotask-Warteschlange plant wann immer eine seiner Abhängigkeiten könnte sich ändernexport function effect(cb) {let destructor;let c = new Signal.Computed(() => { destructor?.(); destructor = cb(); });w.watch(c);c.get( );return () => { destructor?.(); w.unwatch(c) };}
Die Signal-API enthält keine integrierte Funktion wie effect
. Dies liegt daran, dass die Effektplanung subtil ist und oft mit Framework-Rendering-Zyklen und anderen übergeordneten Framework-spezifischen Zuständen oder Strategien verknüpft ist, auf die JS keinen Zugriff hat.
Gehen Sie die verschiedenen hier verwendeten Operationen durch: Der an Watcher
Konstruktor übergebene notify
ist die Funktion, die aufgerufen wird, wenn das Signal von einem „sauberen“ Zustand (wo wir wissen, dass der Cache initialisiert und gültig ist) in einen „geprüften“ oder „schmutzigen“ Zustand übergeht " Zustand (wobei der Cache möglicherweise gültig ist oder nicht, weil mindestens einer der Zustände, von denen dies rekursiv abhängt, geändert wurde).
Aufrufe zur notify
werden letztendlich durch einen Aufruf von .set()
bei einem Zustandssignal ausgelöst. Dieser Aufruf ist synchron: Er erfolgt, bevor .set
zurückkehrt. Es besteht jedoch kein Grund zur Sorge, dass dieser Rückruf das Signaldiagramm in einem halbverarbeiteten Zustand beobachtet, da während eines notify
kein Signal gelesen oder geschrieben werden kann, selbst bei einem untrack
Aufruf. Da notify
während .set()
aufgerufen wird, unterbricht es einen anderen Logikthread, der möglicherweise nicht vollständig ist. Um Signale von notify
zu lesen oder zu schreiben, planen Sie die Ausführung der Arbeit zu einem späteren Zeitpunkt ein, z. B. indem Sie das Signal in eine Liste schreiben, damit später darauf zugegriffen werden kann, oder mit queueMicrotask
wie oben.
Beachten Sie, dass es durchaus möglich ist, Signale ohne Symbol.subtle.Watcher
effektiv zu nutzen, indem die Abfrage berechneter Signale geplant wird, wie es Glimmer tut. Viele Frameworks haben jedoch festgestellt, dass es sehr oft nützlich ist, diese Planungslogik synchron laufen zu lassen, weshalb die Signals-API sie enthält.
Sowohl berechnete als auch Zustandssignale werden wie alle JS-Werte durch Müll gesammelt. Aber Beobachter haben eine besondere Art, Dinge am Leben zu halten: Alle Signale, die von einem Beobachter beobachtet werden, werden am Leben gehalten, solange einer der zugrunde liegenden Zustände erreichbar ist, da diese möglicherweise einen zukünftigen notify
(und dann einen zukünftigen .get()
). Denken Sie aus diesem Grund daran, Watcher.prototype.unwatch
aufzurufen, um Effekte zu bereinigen.
Signal.subtle.untrack
ist eine Notluke, die das Lesen von Signalen ermöglicht, ohne diese Lesevorgänge zu verfolgen. Diese Funktion ist unsicher, da sie die Erstellung berechneter Signale ermöglicht, deren Wert von anderen Signalen abhängt, die jedoch nicht aktualisiert werden, wenn sich diese Signale ändern. Es sollte verwendet werden, wenn die nicht verfolgten Zugriffe das Ergebnis der Berechnung nicht ändern.
Diese Funktionen können später hinzugefügt werden, sind jedoch nicht im aktuellen Entwurf enthalten. Ihre Auslassung ist auf das Fehlen eines etablierten Konsenses im Designbereich zwischen den Frameworks sowie auf die nachgewiesene Fähigkeit zurückzuführen, ihr Fehlen mit Mechanismen zusätzlich zum in diesem Dokument beschriebenen Signalkonzept zu umgehen. Leider schränkt die Auslassung jedoch das Potenzial der Interoperabilität zwischen Frameworks ein. Da Prototypen der in diesem Dokument beschriebenen Signale erstellt werden, wird versucht, erneut zu prüfen, ob diese Auslassungen die richtige Entscheidung waren.
Async : Signale stehen in diesem Modell immer synchron zur Auswertung zur Verfügung. Allerdings ist es häufig nützlich, über bestimmte asynchrone Prozesse zu verfügen, die zum Setzen eines Signals führen, und zu verstehen, wann ein Signal noch „geladen“ wird. Eine einfache Möglichkeit, den Ladezustand zu modellieren, sind Ausnahmen, und das Ausnahme-Caching-Verhalten berechneter Signale lässt sich mit dieser Technik einigermaßen vernünftig gestalten. Verbesserte Techniken werden in Ausgabe Nr. 30 besprochen.
Transaktionen : Für Übergänge zwischen Ansichten ist es oft nützlich, einen Live-Status sowohl für den „Von“- als auch den „Bis“-Status beizubehalten. Der „An“-Zustand wird im Hintergrund gerendert, bis er zum Austauschen (Festschreiben der Transaktion) bereit ist, während der „Von“-Zustand interaktiv bleibt. Um beide Zustände gleichzeitig aufrechtzuerhalten, muss der Zustand des Signaldiagramms „verzweigt“ werden, und es kann sogar nützlich sein, mehrere ausstehende Übergänge gleichzeitig zu unterstützen. Diskussion in Ausgabe Nr. 73.
Auch einige mögliche Convenience-Methoden werden weggelassen.
Dieser Vorschlag steht auf der TC39-Tagesordnung für Stufe 1 im April 2024. Derzeit kann er als „Stufe 0“ betrachtet werden.
Für diesen Vorschlag ist ein Polyfill mit einigen grundlegenden Tests verfügbar. Einige Framework-Autoren haben begonnen, mit der Ersetzung dieser Signalimplementierung zu experimentieren, diese Verwendung befindet sich jedoch noch in einem frühen Stadium.
Die Mitarbeiter des Signal-Vorschlags möchten bei der Weiterentwicklung dieses Vorschlags besonders konservativ vorgehen, damit wir nicht in die Falle tappen, etwas geliefert zu bekommen, das wir am Ende bereuen und nicht wirklich nutzen. Unser Plan besteht darin, die folgenden zusätzlichen Aufgaben zu erledigen, die im TC39-Prozess nicht erforderlich sind, um sicherzustellen, dass dieser Vorschlag auf dem richtigen Weg ist:
Bevor wir Phase 2 vorschlagen, planen wir Folgendes:
Entwickeln Sie mehrere Polyfill-Implementierungen in Produktionsqualität, die solide, gut getestet (z. B. Tests verschiedener Frameworks sowie Tests im test262-Stil) und hinsichtlich der Leistung wettbewerbsfähig sind (wie durch einen gründlichen Signal-/Framework-Benchmark-Satz überprüft).
Integrieren Sie die vorgeschlagene Signal-API in eine große Anzahl von JS-Frameworks, die wir für einigermaßen repräsentativ halten, und einige große Anwendungen arbeiten auf dieser Basis. Testen Sie, ob es in diesen Kontexten effizient und korrekt funktioniert.
Sie verfügen über ein fundiertes Verständnis des Umfangs möglicher Erweiterungen der API und sind zu dem Schluss gekommen, welche (falls vorhanden) in diesen Vorschlag aufgenommen werden sollten.
In diesem Abschnitt werden alle für JavaScript bereitgestellten APIs im Hinblick auf die von ihnen implementierten Algorithmen beschrieben. Dies kann als Proto-Spezifikation betrachtet werden und wird zu diesem frühen Zeitpunkt eingefügt, um einen möglichen Semantiksatz festzulegen, gleichzeitig aber sehr offen für Änderungen zu sein.
Einige Aspekte des Algorithmus:
Die Reihenfolge der Lesevorgänge von Signalen innerhalb einer Berechnung ist von Bedeutung und kann in der Reihenfolge beobachtet werden, in der bestimmte Rückrufe (welcher Watcher
aufgerufen wird, equals
, der erste Parameter für new Signal.Computed
und die watched
/ unwatched
Rückrufe) ausgeführt werden. Das bedeutet, dass die Quellen eines berechneten Signals geordnet gespeichert werden müssen.
Diese vier Rückrufe können alle Ausnahmen auslösen, und diese Ausnahmen werden auf vorhersehbare Weise an den aufrufenden JS-Code weitergegeben. Die Ausnahmen stoppen nicht die Ausführung dieses Algorithmus und belassen das Diagramm nicht in einem halbverarbeiteten Zustand. Bei Fehlern, die im notify
eines Watchers ausgelöst werden, wird diese Ausnahme an den .set()
-Aufruf gesendet, der sie ausgelöst hat, wobei ein AggregateError verwendet wird, wenn mehrere Ausnahmen ausgelöst wurden. Die anderen (einschließlich watched
/ unwatched
?) werden im Wert des Signals gespeichert, um beim Lesen erneut ausgelöst zu werden, und ein solches erneut auslösendes Signal kann wie jedes andere mit einem normalen Wert als ~clean~
markiert werden.
Bei berechneten Signalen, die nicht „beobachtet“ (von irgendeinem Beobachter beobachtet) werden, wird darauf geachtet, Zirkularitäten zu vermeiden, sodass sie unabhängig von anderen Teilen des Signaldiagramms als Müll gesammelt werden können. Intern lässt sich dies mit einem System von immer erfassten Generationsnummern umsetzen; Beachten Sie, dass optimierte Implementierungen möglicherweise auch lokale Generationsnummern pro Knoten umfassen oder die Verfolgung einiger Nummern auf überwachten Signalen vermeiden können.
Signalalgorithmen müssen auf einen bestimmten globalen Zustand verweisen. Dieser Status gilt global für den gesamten Thread oder „Agenten“.
computing
: Das innerste berechnete Signal oder Effektsignal, das derzeit aufgrund eines .get
oder .run
Aufrufs neu ausgewertet wird, oder null
. Anfangs null
.
frozen
: Boolescher Wert, der angibt, ob derzeit ein Rückruf ausgeführt wird, der erfordert, dass das Diagramm nicht geändert wird. Zunächst false
.
generation
: Eine aufsteigende Ganzzahl, beginnend bei 0, die verwendet wird, um zu verfolgen, wie aktuell ein Wert ist, und gleichzeitig Zirkelschlüsse zu vermeiden.
Signal
Namespace Signal
ist ein gewöhnliches Objekt, das als Namensraum für signalbezogene Klassen und Funktionen dient.
Signal.subtle
ist ein ähnliches inneres Namespace-Objekt.
Signal.State
KlasseSignal.State
Slots value
: Der aktuelle Wert des Statussignals
equals
: Die Vergleichsfunktion, die beim Ändern von Werten verwendet wird
watched
: Der Rückruf, der aufgerufen werden soll, wenn das Signal von einem Effekt beobachtet wird
unwatched
: Der Rückruf, der aufgerufen werden soll, wenn das Signal nicht mehr von einem Effekt beobachtet wird
sinks
: Satz überwachter Signale, die von diesem abhängen
Signal.State(initialValue, options)
Setzen Sie value
dieses Signals auf initialValue
.
Setzen Sie equals
dieses Signals auf Optionen?.equals
Stellen Sie watched
dieses Signals auf „Optionen?“ ein.[Signal.subtle.watched]
„Dieses Signal unwatched
“ auf „Optionen?“ setzen.[Signal.subtle.unwatched]
Setzt sinks
dieses Signals auf den leeren Satz
Signal.State.prototype.get()
Wenn frozen
wahr ist, lösen Sie eine Ausnahme aus.
Wenn computing
nicht undefined
ist, fügen Sie dieses Signal zu computing
- sources
hinzu.
HINWEIS: Wir fügen dem sinks
dieses Signals keine computing
hinzu, bis es von einem Beobachter beobachtet wird.
Gibt value
dieses Signals zurück.
Signal.State.prototype.set(newValue)
Wenn der aktuelle Ausführungskontext frozen
ist, wird eine Ausnahme ausgelöst.
Führen Sie den Algorithmus „Signalwert festlegen“ mit diesem Signal und dem ersten Parameter für den Wert aus.
Wenn dieser Algorithmus ~clean~
zurückgegeben hat, dann geben Sie undefiniert zurück.
Setzen Sie den state
aller sinks
dieses Signals auf (wenn es ein berechnetes Signal ist) ~dirty~
wenn sie zuvor sauber waren, oder (wenn es ein Watcher ist) ~pending~
wenn es zuvor ~watching~
war.
Setzen Sie den state
aller berechneten Signalabhängigkeiten der Senken (rekursiv) auf ~checked~
wenn sie zuvor ~clean~
waren (d. h. schmutzige Markierungen bleiben bestehen), oder für Beobachter ~pending~
wenn sie zuvor ~watching~
waren.
Für jeden zuvor ~watching~
Beobachter, der bei dieser rekursiven Suche angetroffen wurde, dann in der Reihenfolge der Tiefe zuerst:
Setzen Sie frozen
auf „true“.
Rufen Sie ihren notify
Rückruf auf (wobei alle ausgelösten Ausnahmen beiseite bleiben, aber der Rückgabewert von notify
ignoriert wird).
frozen
auf „falsch“ wiederherstellen.
Setzen Sie den state
des Watchers auf ~waiting~
.
Wenn von den notify
eine Ausnahme ausgelöst wurde, geben Sie diese an den Aufrufer weiter, nachdem alle notify
ausgeführt wurden. Wenn es mehrere Ausnahmen gibt, packen Sie sie in einen AggregateError und lösen Sie diesen aus.
Rückgabe undefiniert.
Signal.Computed
-KlasseSignal.Computed
Zustandsmaschine Der state
eines berechneten Signals kann einer der folgenden sein:
~clean~
: Der Wert des Signals ist vorhanden und bekanntermaßen nicht veraltet.
~checked~
: Eine (indirekte) Quelle dieses Signals hat sich geändert; Dieses Signal hat einen Wert, kann aber veraltet sein. Ob es veraltet ist oder nicht, wird erst bekannt, wenn alle unmittelbaren Quellen ausgewertet wurden.
~computing~
: Der Rückruf dieses Signals wird derzeit als Nebeneffekt eines .get()
-Aufrufs ausgeführt.
~dirty~
: Entweder hat dieses Signal einen Wert, von dem bekannt ist, dass er veraltet ist, oder er wurde nie ausgewertet.
Das Übergangsdiagramm sieht wie folgt aus:
stateDiagram-v2
[*] -> schmutzig
dirty -> Berechnung: [4]
Computing -> sauber: [5]
sauber -> schmutzig: [2]
sauber --> überprüft: [3]
geprüft --> sauber: [6]
geprüft --> schmutzig: [1]
LadenDie Übergänge sind:
Nummer | Aus | Zu | Zustand | Algorithmus |
---|---|---|---|---|
1 | ~checked~ | ~dirty~ | Eine unmittelbare Quelle dieses Signals, bei dem es sich um ein berechnetes Signal handelt, wurde ausgewertet und sein Wert hat sich geändert. | Algorithmus: berechnetes Signal neu berechnen |
2 | ~clean~ | ~dirty~ | Eine unmittelbare Quelle dieses Signals, bei der es sich um einen Zustand handelt, wurde mit einem Wert festgelegt, der nicht mit dem vorherigen Wert übereinstimmt. | Methode: Signal.State.prototype.set(newValue) |
3 | ~clean~ | ~checked~ | Eine rekursive, aber nicht unmittelbare Quelle dieses Signals, bei der es sich um einen Zustand handelt, wurde mit einem Wert festgelegt, der nicht mit dem vorherigen Wert übereinstimmt. | Methode: Signal.State.prototype.set(newValue) |
4 | ~dirty~ | ~computing~ | Wir sind dabei, den callback auszuführen. | Algorithmus: berechnetes Signal neu berechnen |
5 | ~computing~ | ~clean~ | Der callback hat die Auswertung abgeschlossen und entweder einen Wert zurückgegeben oder eine Ausnahme ausgelöst. | Algorithmus: berechnetes Signal neu berechnen |
6 | ~checked~ | ~clean~ | Alle unmittelbaren Quellen dieses Signals wurden ausgewertet und alle wurden als unverändert entdeckt, sodass wir jetzt wissen, dass sie nicht veraltet sind. | Algorithmus: berechnetes Signal neu berechnen |
Signal.Computed
Interne Steckplätze value
: Der zuvor zwischengespeicherte Wert des Signals oder ~uninitialized~
für ein nie gelesenes berechnetes Signal. Der Wert kann eine Ausnahme sein, die erneut ausgelöst wird, wenn der Wert gelesen wird. Für Effektsignale immer undefined
.
state
: Kann ~clean~
, ~checked~
, ~computing~
oder ~dirty~
sein.
sources
: Eine geordnete Menge von Signalen, von denen dieses Signal abhängt.
sinks
: Eine geordnete Menge von Signalen, die von diesem Signal abhängen.
equals
: Die in den Optionen bereitgestellte Methode equal.
callback
: Der Rückruf, der aufgerufen wird, um den berechneten Signalwert abzurufen. Auf den ersten Parameter setzen, der an den Konstruktor übergeben wird.
Signal.Computed
KonstruktorDer Konstruktor legt fest
callback
auf seinen ersten Parameter
equals
basierend auf Optionen, standardmäßig wird Object.is
verwendet, wenn es nicht vorhanden ist
state
zu ~dirty~
value
auf ~uninitialized~
Mit AsyncContext schließt der an new Signal.Computed
übergebene Rückruf den Snapshot ab dem Zeitpunkt, als der Konstruktor aufgerufen wurde, und stellt diesen Snapshot während seiner Ausführung wieder her.
Signal.Computed.prototype.get
Wenn der aktuelle Ausführungskontext frozen
ist oder wenn dieses Signal den Status ~computing~
hat oder wenn dieses Signal ein Effekt ist und ein berechnetes Signal computing
, wird eine Ausnahme ausgelöst.
Wenn computing
nicht null
ist, fügen Sie dieses Signal zum sources
von computing
hinzu.
HINWEIS: Wir fügen dem sinks
dieses Signals erst dann computing
hinzu, wenn es von einem Beobachter beobachtet wird.
Wenn der Status dieses Signals ~dirty~
oder ~checked~
ist: Wiederholen Sie die folgenden Schritte, bis dieses Signal ~clean~
ist:
Rekursieren Sie über sources
nach oben, um die tiefste, am weitesten links stehende (d. h. am frühesten beobachtete) rekursive Quelle zu finden, bei der es sich um ein berechnetes Signal mit der Markierung ~dirty~
handelt (die Suche wird abgebrochen, wenn ein ~clean~
berechnetes Signal gefunden wird, und dieses berechnete Signal wird als letztes eingefügt). suchen).
Führen Sie den Algorithmus „Verschmutztes berechnetes Signal neu berechnen“ für dieses Signal aus.
Zu diesem Zeitpunkt ist der Status dieses Signals ~clean~
und keine rekursiven Quellen werden ~dirty~
oder ~checked~
sein. Gibt den value
des Signals zurück. Wenn der Wert eine Ausnahme ist, lösen Sie diese Ausnahme erneut aus.
Signal.subtle.Watcher
-KlasseSignal.subtle.Watcher
Zustandsmaschine Der state
eines Watchers kann einer der folgenden sein:
~waiting~
: Der notify
wurde ausgeführt oder der Watcher ist neu, beobachtet aber keine Signale aktiv.
~watching~
: Der Watcher beobachtet aktiv Signale, es sind jedoch noch keine Änderungen eingetreten, die einen notify
erforderlich machen würden.
~pending~
: Eine Abhängigkeit des Watchers hat sich geändert, aber der notify
wurde noch nicht ausgeführt.
Das Übergangsdiagramm sieht wie folgt aus:
stateDiagram-v2
[*] -> warten
warten --> zuschauen: [1]
Beobachten --> Warten: [2]
Beobachten -> ausstehend: [3]
ausstehend -> wartend: [4]
LadenDie Übergänge sind:
Nummer | Aus | Zu | Zustand | Algorithmus |
---|---|---|---|---|
1 | ~waiting~ | ~watching~ | Die watch -Methode des Watchers wurde aufgerufen. | Methode: Signal.subtle.Watcher.prototype.watch(...signals) |
2 | ~watching~ | ~waiting~ | Die unwatch -Methode des Watchers wurde aufgerufen und das zuletzt beobachtete Signal wurde entfernt. | Methode: Signal.subtle.Watcher.prototype.unwatch(...signals) |
3 | ~watching~ | ~pending~ | Ein beobachtetes Signal hat möglicherweise seinen Wert geändert. | Methode: Signal.State.prototype.set(newValue) |
4 | ~pending~ | ~waiting~ | Der notify wurde ausgeführt. | Methode: Signal.State.prototype.set(newValue) |
Signal.subtle.Watcher
Slots state
: Kann ~watching~
, ~pending~
oder ~waiting~
sein
signals
: Ein geordneter Satz von Signalen, die dieser Beobachter beobachtet
notifyCallback
: Der Rückruf, der aufgerufen wird, wenn sich etwas ändert. Auf den ersten Parameter setzen, der an den Konstruktor übergeben wird.
new Signal.subtle.Watcher(callback)
state
ist auf ~waiting~
gesetzt.
signals
als leere Menge initialisieren.
notifyCallback
wird auf den Callback-Parameter gesetzt.
Mit AsyncContext schließt der an new Signal.subtle.Watcher
übergebene Rückruf nicht den Snapshot ab dem Zeitpunkt, als der Konstruktor aufgerufen wurde, sodass Kontextinformationen rund um den Schreibvorgang sichtbar sind.
Signal.subtle.Watcher.prototype.watch(...signals)
Wenn frozen
wahr ist, lösen Sie eine Ausnahme aus.
Wenn eines der Argumente kein Signal ist, lösen Sie eine Ausnahme aus.
Hängen Sie alle Argumente an das Ende der signals
dieses Objekts an.
Für jedes neu gesehene Signal, in der Reihenfolge von links nach rechts,
Fügen Sie diesen Beobachter als sink
zu diesem Signal hinzu.
Wenn dies die erste Senke war, führen Sie eine Rekursion nach oben zu den Quellen durch, um dieses Signal als Senke hinzuzufügen.
Setzen Sie frozen
auf „true“.
Rufen Sie den watched
Rückruf auf, falls vorhanden.
frozen
auf „falsch“ wiederherstellen.
Wenn der state
~waiting~
ist, setzen Sie ihn auf ~watching~
.
Signal.subtle.Watcher.prototype.unwatch(...signals)
Wenn frozen
wahr ist, lösen Sie eine Ausnahme aus.
Wenn eines der Argumente kein Signal ist oder nicht von diesem Beobachter überwacht wird, lösen Sie eine Ausnahme aus.
Für jedes Signal in den Argumenten, in der Reihenfolge von links nach rechts,
Entferne dieses Signal aus signals
dieses Wächters.
Entferne diesen Wächter aus dem sink
-Set dieses Signals.
Wenn sink
dieses Signals leer geworden ist, entfernen Sie dieses Signal als Senke aus jeder seiner Quellen.
Setzen Sie frozen
auf „true“.
Rufen Sie den unwatched
Rückruf auf, falls vorhanden.
frozen
auf „falsch“ wiederherstellen.
Wenn der Watcher jetzt keine signals
hat und sich im state
~watching~
befindet, setzen Sie ihn auf ~waiting~
.
Signal.subtle.Watcher.prototype.getPending()
Gibt ein Array zurück, das die Teilmenge der signals
enthält, bei denen es sich um berechnete Signale im Status ~dirty~
oder ~pending~
handelt.
Signal.subtle.untrack(cb)
Sei c
der aktuelle computing
des Ausführungskontexts.
computing
auf Null setzen.
Rufen Sie cb
.
Stellen Sie computing
auf c
wieder her (auch wenn cb
eine Ausnahme ausgelöst hat).
Gibt den Rückgabewert von cb
zurück (wobei jede Ausnahme erneut ausgelöst wird).
Hinweis: Untrack führt Sie nicht aus dem frozen
Zustand heraus, der strikt beibehalten wird.
Signal.subtle.currentComputed()
Gibt den aktuellen computing
zurück.
Bereinigen Sie sources
dieses Signals und entfernen Sie es aus den sinks
dieser Quellen.
Speichern Sie den vorherigen computing
und stellen Sie computing
auf dieses Signal ein.
Setzen Sie den Status dieses Signals auf ~computing~
.
Führen Sie den Rückruf dieses berechneten Signals aus und verwenden Sie dieses Signal als diesen Wert. Speichern Sie den Rückgabewert. Wenn der Rückruf eine Ausnahme ausgelöst hat, speichern Sie diese zum erneuten Auslösen.
Stellen Sie den vorherigen computing
wieder her.
Wenden Sie den Algorithmus „Signalwert festlegen“ auf den Rückgabewert des Rückrufs an.
Setzen Sie den Status dieses Signals auf ~clean~
.
Wenn dieser Algorithmus ~dirty~
zurückgegeben hat: Markieren Sie alle Senken dieses Signals als ~dirty~
(zuvor waren die Senken möglicherweise eine Mischung aus geprüft und schmutzig). (Oder, wenn dies nicht beobachtet wird, nehmen Sie eine neue Generationsnummer an, um auf Schmutz oder ähnliches hinzuweisen.)
Andernfalls hat dieser Algorithmus ~clean~
zurückgegeben: Wenn in diesem Fall für jede ~checked~
Senke dieses Signals alle Quellen dieses Signals jetzt sauber sind, markieren Sie dieses Signal ebenfalls als ~clean~
. Wenden Sie diesen Bereinigungsschritt rekursiv auf weitere Senken an, auf alle neu bereinigten Signale, die Senken überprüft haben. (Oder, wenn dies unbeobachtet ist, geben Sie es irgendwie an, damit die Bereinigung langsam voranschreiten kann.)
Wenn diesem Algorithmus ein Wert übergeben wurde (im Gegensatz zu einer Ausnahme zum erneuten Auslösen, vom Algorithmus zur Neuberechnung des schmutzig berechneten Signals):
Rufen Sie die Funktion equals
dieses Signals auf und übergeben Sie als Parameter den aktuellen value
, den neuen Wert und dieses Signal. Wenn eine Ausnahme ausgelöst wird, speichern Sie diese Ausnahme (zum erneuten Auslösen beim Lesen) als Wert des Signals und fahren Sie fort, als ob der Rückruf „false“ zurückgegeben hätte.
Wenn diese Funktion „true“ zurückgegeben hat, geben Sie ~clean~
zurück.
Legen Sie den value
dieses Signals auf den Parameter fest.
Rückkehr ~dirty~
F : Ist es nicht etwas zu früh, etwas im Zusammenhang mit Signalen zu standardisieren, wenn diese gerade erst begonnen haben, im Jahr 2022 die heiße Neuheit zu sein? Sollten wir ihnen nicht mehr Zeit geben, sich zu entwickeln und zu stabilisieren?
A : Der aktuelle Stand von Signals in Web-Frameworks ist das Ergebnis von mehr als 10 Jahren kontinuierlicher Entwicklung. Mit steigenden Investitionen, wie es in den letzten Jahren der Fall war, nähern sich fast alle Web-Frameworks einem sehr ähnlichen Kernmodell von Signals an. Dieser Vorschlag ist das Ergebnis einer gemeinsamen Designübung einer großen Anzahl derzeit führender Anbieter von Web-Frameworks und wird ohne die Validierung dieser Gruppe von Domänenexperten in verschiedenen Kontexten nicht zur Standardisierung vorangetrieben.
F : Können integrierte Signale aufgrund ihrer engen Integration mit Rendering und Besitz überhaupt von Frameworks verwendet werden?
A : Die Teile, die eher Framework-spezifisch sind, liegen tendenziell im Bereich der Effekte, der Planung und des Besitzes/der Verfügung, die dieser Vorschlag nicht zu lösen versucht. Unsere erste Priorität bei Signalen zur Prototypisierung von Standards besteht darin, zu validieren, dass sie kompatibel und mit guter Leistung „unter“ bestehenden Frameworks sitzen können.
F : Soll die Signal-API direkt von Anwendungsentwicklern verwendet oder von Frameworks umschlossen werden?
A : Obwohl diese API direkt von Anwendungsentwicklern verwendet werden könnte (zumindest der Teil, der nicht im Signal.subtle
-Namespace liegt), ist sie nicht besonders ergonomisch gestaltet. Stattdessen stehen die Bedürfnisse der Bibliotheks-/Framework-Autoren im Vordergrund. Von den meisten Frameworks wird erwartet, dass sie sogar die grundlegenden Signal.State
und Signal.Computed
-APIs mit etwas umhüllen, das ihre ergonomische Ausrichtung zum Ausdruck bringt. In der Praxis ist es in der Regel am besten, Signale über ein Framework zu verwenden, das schwierigere Funktionen verwaltet (z. B. Watcher, untrack
) sowie Besitz und Verfügung verwaltet (z. B. herausfinden, wann Signale zu Watchern hinzugefügt und von diesen entfernt werden sollten) und Planen des Renderings in DOM – dieser Vorschlag versucht nicht, diese Probleme zu lösen.
F : Muss ich Signale im Zusammenhang mit einem Widget entfernen, wenn dieses Widget zerstört wird? Was ist die API dafür?
A : Der relevante Teardown-Vorgang hier ist Signal.subtle.Watcher.prototype.unwatch
. Nur beobachtete Signale müssen bereinigt werden (indem man sie nicht mehr beobachtet), während nicht beobachtete Signale automatisch im Garbage Collection gesammelt werden können.
F : Funktionieren Signale mit VDOM oder direkt mit dem zugrunde liegenden HTML-DOM?
A : Ja! Signale sind unabhängig von der Rendering-Technologie. Bestehende JavaScript-Frameworks, die signalähnliche Konstrukte verwenden, integrieren sich in VDOM (z. B. Preact), das native DOM (z. B. Solid) und eine Kombination (z. B. Vue). Dasselbe wird mit integrierten Signalen möglich sein.
F : Wird es ergonomisch sein, Signale im Kontext klassenbasierter Frameworks wie Angular und Lit zu verwenden? Was ist mit Compiler-basierten Frameworks wie Svelte?
A : Klassenfelder können mit einem einfachen Accessor-Decorator signalbasiert gemacht werden, wie in der Signal-Polyfill-Readme-Datei gezeigt. Signale sind sehr eng an die Runen von Svelte 5 angelehnt – es ist für einen Compiler einfach, Runen in die hier definierte Signal-API umzuwandeln, und genau das macht Svelte 5 intern (allerdings mit seiner eigenen Signalbibliothek).
F : Funktionieren Signale mit SSR? Flüssigkeitszufuhr? Wiederaufnahmefähigkeit?
A : Ja. Qwik nutzt Signale mit gutem Erfolg bei beiden Eigenschaften, und andere Frameworks verfügen über andere gut entwickelte Ansätze zur Hydratation mit Signalen mit unterschiedlichen Kompromissen. Wir glauben, dass es möglich ist, die wiederaufnehmbaren Signale von Qwik mithilfe eines miteinander verknüpften Zustandssignals und eines berechneten Signals zu modellieren, und planen, dies im Code zu beweisen.
F : Funktionieren Signale mit einem unidirektionalen Datenfluss wie React?
A : Ja, Signale sind ein Mechanismus für den unidirektionalen Datenfluss. Mit signalbasierten UI-Frameworks können Sie Ihre Ansicht als Funktion des Modells ausdrücken (wobei das Modell Signale enthält). Ein Graph aus Zustand und berechneten Signalen ist konstruktionsbedingt azyklisch. Es ist auch möglich, React-Antimuster innerhalb von Signalen (!) neu zu erstellen, z. B. besteht das Signaläquivalent eines setState
innerhalb von useEffect
darin, einen Watcher zu verwenden, um einen Schreibvorgang in ein State-Signal zu planen.
F : In welcher Beziehung stehen Signale zu Zustandsverwaltungssystemen wie Redux? Fördern Signale einen unstrukturierten Zustand?
A : Signale können eine effiziente Grundlage für speicherähnliche Zustandsverwaltungsabstraktionen bilden. Ein häufiges Muster, das in mehreren Frameworks zu finden ist, ist ein Objekt, das auf einem Proxy basiert, der intern Eigenschaften mithilfe von Signalen darstellt, z. B. Vue reactive()
oder Solid Stores. Diese Systeme ermöglichen eine flexible Gruppierung von Zuständen auf der richtigen Abstraktionsebene für die jeweilige Anwendung.
F : Was bietet Signals, das Proxy
derzeit nicht unterstützt?
A : Proxys und Signale ergänzen sich und passen gut zusammen. Mit Proxys können Sie flache Objektoperationen abfangen und Signale koordinieren, um ein Abhängigkeitsdiagramm (von Zellen) zu koordinieren. Die Unterstützung eines Proxys mit Signalen ist eine großartige Möglichkeit, eine verschachtelte reaktive Struktur mit hervorragender Ergonomie zu erstellen.
In diesem Beispiel können wir einen Proxy verwenden, um dem Signal eine Getter- und Setter-Eigenschaft zu verleihen, anstatt die Methoden get
und set
zu verwenden:
const a = new Signal.State(0);const b = new Proxy(a, { get(target, property, Receiver) {if (property === 'value') { return target.get():} } set(target, property, value, Receiver) {if (property === 'value') { target.set(value)!} }});// Verwendung in einem hypothetischen reaktiven Kontext:<template> {b.value} <button onclick={() => {b.value++; }}>Ändern</button></template>
Wenn Sie einen Renderer verwenden, der für feinkörnige Reaktivität optimiert ist, wird durch Klicken auf die Schaltfläche die Zelle b.value
aktualisiert.
Sehen:
Beispiele für verschachtelte reaktive Strukturen, die sowohl mit Signalen als auch mit Proxys erstellt wurden: signal-utils
Beispiele für frühere Implementierungen, die die Beziehung zwischen reaktiven Daten und Proxys zeigen: verfolgte integrierte Funktionen
Diskussion.
F : Sind Signale Push-basiert oder Pull-basiert?
A : Die Auswertung berechneter Signale erfolgt pull-basiert: Berechnete Signale werden nur ausgewertet, wenn .get()
aufgerufen wird, auch wenn sich der zugrunde liegende Zustand viel früher geändert hat. Gleichzeitig kann die Änderung eines Statussignals sofort den Rückruf eines Beobachters auslösen und so die Benachrichtigung „pushen“. Daher kann man sich Signale als eine „Push-Pull“-Konstruktion vorstellen.
F : Führen Signale Nichtdeterminismus in die JavaScript-Ausführung ein?
A : Nein. Zum einen haben alle Signaloperationen eine genau definierte Semantik und Reihenfolge und unterscheiden sich nicht zwischen konformen Implementierungen. Auf einer höheren Ebene folgen Signale einem bestimmten Satz von Invarianten, in Bezug auf die sie „solide“ sind. Ein berechnetes Signal beobachtet den Signalgraphen immer in einem konsistenten Zustand und seine Ausführung wird nicht durch anderen Signal-mutierenden Code unterbrochen (mit Ausnahme von Dingen, die es sich selbst nennt). Siehe Beschreibung oben.
F : Wann ist die Aktualisierung des berechneten Signals geplant, wenn ich in ein Statussignal schreibe?
A : Es ist nicht geplant! Das berechnete Signal wird sich selbst neu berechnen, wenn es das nächste Mal jemand liest. Synchron kann ein Watcher- notify
aufgerufen werden, der es Frameworks ermöglicht, einen Lesevorgang zu dem Zeitpunkt zu planen, den sie für angemessen halten.
F : Wann werden Schreibvorgänge in Zustandssignale wirksam? Sofort oder gebündelt?
A : Schreibvorgänge in Zustandssignale werden sofort widergespiegelt – wenn ein berechnetes Signal, das vom Zustandssignal abhängt, das nächste Mal gelesen wird, berechnet es sich bei Bedarf selbst neu, auch wenn es sich in der unmittelbar folgenden Codezeile befindet. Die diesem Mechanismus innewohnende Trägheit (dass berechnete Signale nur berechnet werden, wenn sie gelesen wird) führt jedoch dazu, dass die Berechnungen in der Praxis möglicherweise stapelweise erfolgen.
F : Was bedeutet es für Signale, eine „störungsfreie“ Ausführung zu ermöglichen?
A : Frühere Push-basierte Modelle für Reaktivität hatten das Problem redundanter Berechnungen: Wenn eine Aktualisierung eines Statussignals dazu führt, dass das berechnete Signal eifrig ausgeführt wird, kann dies letztendlich zu einer Aktualisierung der Benutzeroberfläche führen. Dieses Schreiben in die Benutzeroberfläche könnte jedoch verfrüht sein, wenn es vor dem nächsten Frame zu einer weiteren Änderung des Ursprungszustandssignals kommen würde. Manchmal wurden Endbenutzern aufgrund solcher Störungen sogar ungenaue Zwischenwerte angezeigt. Signale vermeiden diese Dynamik, indem sie Pull-basiert und nicht Push-basiert sind: Wenn das Framework das Rendern der Benutzeroberfläche plant, ruft es die entsprechenden Aktualisierungen ab und vermeidet so verschwendete Arbeit sowohl bei der Berechnung als auch beim Schreiben in das DOM.
F : Was bedeutet es, dass Signale „verlustbehaftet“ sind?
A : Das ist die Kehrseite einer störungsfreien Ausführung: Signale stellen eine Datenzelle dar – nur den unmittelbaren aktuellen Wert (der sich ändern kann), keinen Datenstrom im Laufe der Zeit. Wenn Sie also zweimal hintereinander in ein Zustandssignal schreiben, ohne etwas anderes zu tun, geht der erste Schreibvorgang „verloren“ und wird von keinem berechneten Signal oder Effekt gesehen. Dabei handelt es sich eher um eine Funktion als um einen Fehler – andere Konstrukte (z. B. asynchrone Iterables, Observables) sind für Streams besser geeignet.
F : Werden native Signale schneller sein als bestehende JS-Signal-Implementierungen?
A : Wir hoffen es (um einen kleinen konstanten Faktor), aber dies muss noch im Code bewiesen werden. JS-Engines sind keine Zauberei und müssen letztendlich die gleichen Arten von Algorithmen implementieren wie JS-Implementierungen von Signalen. Siehe Abschnitt oben zur Leistung.
F : Warum enthält dieser Vorschlag keine effect()
-Funktion, wenn doch Effekte für die praktische Nutzung von Signalen notwendig sind?
A : Die Auswirkungen hängen von Natur aus mit der Planung und Entsorgung zusammen, die durch Frameworks verwaltet werden und nicht in den Geltungsbereich dieses Vorschlags fallen. Stattdessen enthält dieser Vorschlag die Grundlage für die Implementierung von Effekten über die Signal.subtle.Watcher
-API auf niedrigerer Ebene.
F : Warum erfolgen Abonnements automatisch, anstatt eine manuelle Schnittstelle bereitzustellen?
A : Die Erfahrung hat gezeigt, dass manuelle Abonnementschnittstellen für Reaktivität unergonomisch und fehleranfällig sind. Die automatische Verfolgung ist einfacher zusammensetzbar und eine Kernfunktion von Signals.
F : Warum läuft der Watcher
-Rückruf synchron und nicht in einer Mikrotask geplant?
A : Da der Rückruf keine Signale lesen oder schreiben kann, entstehen keine Unzulänglichkeiten, wenn er synchron aufgerufen wird. Ein typischer Rückruf fügt einem Array ein Signal hinzu, das später gelesen werden soll, oder markiert irgendwo ein Bit. Es ist unnötig und unpraktisch teuer, für alle diese Arten von Aktionen einen separaten Mikrotask zu erstellen.
F : Dieser API fehlen einige nette Dinge, die mein Lieblings-Framework bietet, was die Programmierung mit Signalen einfacher macht. Kann man das auch zum Standard hinzufügen?
A : Vielleicht. Verschiedene Erweiterungen werden noch geprüft. Bitte reichen Sie ein Problem ein, um eine Diskussion über fehlende Funktionen anzustoßen, die Sie für wichtig halten.
F : Kann die Größe oder Komplexität dieser API reduziert werden?
A : Es ist definitiv ein Ziel, diese API minimal zu halten, und wir haben versucht, dies mit dem oben Dargelegten zu erreichen. Wenn Sie Ideen für weitere Dinge haben, die entfernt werden können, reichen Sie bitte ein Problem zur Diskussion ein.
F : Sollten wir die Standardisierungsarbeit in diesem Bereich nicht mit einem primitiveren Konzept beginnen, beispielsweise mit Observablen?
A : Observables mögen für manche Dinge eine gute Idee sein, aber sie lösen nicht die Probleme, die Signale lösen sollen. Wie oben beschrieben, stellen Observables oder andere Publish/Subscribe-Mechanismen keine vollständige Lösung für viele Arten der UI-Programmierung dar, da unter anderem zu viel fehleranfällige Konfigurationsarbeit für Entwickler erforderlich ist und Arbeit aufgrund mangelnder Faulheit verschwendet wird.
F : Warum werden Signale in TC39 und nicht in DOM vorgeschlagen, wenn man bedenkt, dass die meisten Anwendungen davon webbasiert sind?
A : Einige Mitautoren dieses Vorschlags sind an Nicht-Web-UI-Umgebungen als Ziel interessiert, aber heutzutage könnte jeder Veranstaltungsort dafür geeignet sein, da Web-APIs häufiger außerhalb des Webs implementiert werden. Letztendlich müssen Signale nicht von irgendwelchen DOM-APIs abhängig sein, sodass beide Möglichkeiten funktionieren. Wenn jemand einen triftigen Grund für den Wechsel dieser Gruppe hat, teilen Sie uns dies bitte in einem Problem mit. Derzeit haben alle Mitwirkenden die TC39-Vereinbarungen zum geistigen Eigentum unterzeichnet und es ist geplant, diese TC39 vorzulegen.
F : Wie lange wird es dauern, bis ich Standardsignale verwenden kann?
A : Ein Polyfill ist bereits verfügbar, aber es ist am besten, sich nicht auf seine Stabilität zu verlassen, da sich diese API während des Überprüfungsprozesses weiterentwickelt. In einigen Monaten oder einem Jahr sollte ein qualitativ hochwertiges, hochleistungsfähiges, stabiles Polyfill verwendbar sein, aber dies wird noch Gegenstand von Ausschussüberarbeitungen sein und ist noch nicht Standard. Dem typischen Verlauf eines TC39-Vorschlags folgend, wird es voraussichtlich mindestens zwei bis drei Jahre dauern, bis Signals nativ in allen Browsern verfügbar ist, die einige Versionen zurückreichen, sodass keine Polyfills erforderlich sind.
F : Wie können wir verhindern, dass die falsche Art von Signalen zu früh standardisiert wird, so wie {{JS/Web-Funktion, die Ihnen nicht gefällt}}?
A : Die Autoren dieses Vorschlags planen, beim Prototyping und Testen noch einen Schritt weiter zu gehen, bevor sie bei TC39 eine Weiterentwicklung beantragen. Siehe oben „Status und Entwicklungsplan“. Wenn Sie Lücken in diesem Plan oder Verbesserungsmöglichkeiten sehen, reichen Sie bitte eine Problembegründung ein.