Die Zustandsverwaltung ist einer der wichtigsten Teile einer Webanwendung. Von der Verwendung globaler Variablen über React-Hooks bis hin zur Verwendung von Bibliotheken von Drittanbietern wie MobX, Redux oder zuverlässige und effiziente Anwendung.
Heute schlage ich vor, eine Mini-Zustandsverwaltungsbibliothek in weniger als 50 Zeilen JavaScript zu erstellen, die auf dem Konzept der Observablen basiert. Dieses kann sicherlich unverändert für kleine Projekte verwendet werden, aber über diese pädagogische Übung hinaus empfehle ich Ihnen dennoch, für Ihre realen Projekte auf standardisiertere Lösungen zurückzugreifen.
Wenn Sie ein neues Bibliotheksprojekt starten, ist es wichtig, von Anfang an zu definieren, wie seine API aussehen könnte, um sein Konzept einzufrieren und seine Entwicklung zu steuern, bevor überhaupt über technische Implementierungsdetails nachgedacht wird. Für ein echtes Projekt ist es sogar möglich, zu diesem Zeitpunkt mit dem Schreiben von Tests zu beginnen, um die Implementierung der Bibliothek zu validieren, während sie gemäß einem TDD-Ansatz geschrieben wird.
Hier möchten wir eine einzelne Klasse exportieren, die wir State nennen und die mit einem Objekt instanziiert wird, das den Anfangszustand und eine einzelne Beobachtungsmethode enthält, die es uns ermöglicht, Zustandsänderungen mit Beobachtern zu abonnieren. Diese Beobachter sollten nur ausgeführt werden, wenn sich eine ihrer Abhängigkeiten geändert hat.
Um den Status zu ändern, möchten wir die Klasseneigenschaften direkt verwenden, anstatt eine Methode wie setState zu verwenden.
Da ein Codeausschnitt mehr sagt als tausend Worte, könnte unsere endgültige Implementierung im Einsatz wie folgt aussehen:
const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 1 // Text changed Hello, world! // Count changed 2
Beginnen wir mit der Erstellung einer State-Klasse, die einen Anfangszustand in ihrem Konstruktor akzeptiert und eine Beobachtungsmethode bereitstellt, die wir später implementieren werden.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; } observe(observer) { this.observers.push(observer); } }
Hier entscheiden wir uns für die Verwendung eines internen Zwischenzustandsobjekts, das es uns ermöglicht, die Zustandswerte beizubehalten. Wir speichern die Beobachter auch in einem internen Beobachter-Array, das nützlich sein wird, wenn wir diese Implementierung abschließen.
Da diese beiden Eigenschaften nur innerhalb dieser Klasse verwendet werden, könnten wir sie mit etwas syntaktischem Zucker als privat deklarieren, indem wir ihnen ein # voranstellen und eine Anfangsdeklaration für die Klasse hinzufügen:
class State { #state = {}; #observers = []; constructor(initialState = {}) { this.#state = initialState; this.#observers = []; } observe(observer) { this.#observers.push(observer); } }
Im Prinzip wäre dies eine gute Vorgehensweise, aber wir werden im nächsten Schritt Proxys verwenden und diese sind nicht mit privaten Eigenschaften kompatibel. Ohne ins Detail zu gehen und um diese Implementierung zu vereinfachen, werden wir vorerst öffentliche Eigenschaften verwenden.
Als wir die Spezifikationen für dieses Projekt skizzierten, wollten wir auf die Zustandswerte direkt auf der Klasseninstanz zugreifen und nicht als Eintrag zu ihrem internen Zustandsobjekt.
Dazu verwenden wir ein Proxy-Objekt, das bei der Initialisierung der Klasse zurückgegeben wird.
Wie der Name schon sagt, können Sie mit einem Proxy einen Vermittler für ein Objekt erstellen, um bestimmte Vorgänge, einschließlich seiner Getter und Setter, abzufangen. In unserem Fall erstellen wir einen Proxy, der einen ersten Getter verfügbar macht, der es uns ermöglicht, die Eingaben des Statusobjekts verfügbar zu machen, als ob sie direkt zur State-Instanz gehören würden.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0
Jetzt können wir beim Instanziieren von State ein Anfangszustandsobjekt definieren und seine Werte dann direkt von dieser Instanz abrufen. Sehen wir uns nun an, wie man seine Daten manipuliert.
Wir haben einen Getter hinzugefügt, daher besteht der nächste logische Schritt darin, einen Setter hinzuzufügen, der es uns ermöglicht, das Statusobjekt zu manipulieren.
Wir prüfen zunächst, ob der Schlüssel zu diesem Objekt gehört, prüfen dann, ob sich der Wert tatsächlich geändert hat, um unnötige Aktualisierungen zu verhindern, und aktualisieren schließlich das Objekt mit dem neuen Wert.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); console.log(state.count); // 0 state.count = 1; console.log(state.count); // 1
Wir sind jetzt mit dem Lesen und Schreiben der Daten fertig. Wir können den Statuswert ändern und diese Änderung dann abrufen. Bisher ist unsere Implementierung nicht sehr nützlich, also implementieren wir jetzt Beobachter.
Wir haben bereits ein Array mit den in unserer Instanz deklarierten Beobachterfunktionen, also müssen wir sie nur einzeln aufrufen, wenn sich ein Wert geändert hat.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach((observer) => { observer(this.state); }); } } else { target[prop] = value; } }, }); } observe(observer) { this.observers.push(observer); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.count = 1; state.text = 'Hello, world!'; // Output: // Count changed 1 // Text changed // Count changed 1 // Text changed Hello, world!
Super, wir reagieren jetzt auf Datenänderungen!
Kleines Problem. Falls Sie bisher aufgepasst haben: Ursprünglich wollten wir die Beobachter nur dann ausführen, wenn sich eine ihrer Abhängigkeiten ändert. Wenn wir diesen Code jedoch ausführen, sehen wir, dass jeder Beobachter jedes Mal ausgeführt wird, wenn ein Teil des Status geändert wird.
Aber wie können wir dann die Abhängigkeiten dieser Funktionen identifizieren?
Wieder einmal kommen uns Proxies zu Hilfe. Um die Abhängigkeiten unserer Beobachterfunktionen zu identifizieren, können wir einen Proxy unseres Statusobjekts erstellen, sie damit als Argument ausführen und notieren, auf welche Eigenschaften sie zugegriffen haben.
Einfach, aber effektiv.
Wenn wir Beobachter aufrufen, müssen wir lediglich prüfen, ob sie eine Abhängigkeit von der aktualisierten Eigenschaft haben und sie nur dann auslösen, wenn dies der Fall ist.
Hier ist die endgültige Implementierung unserer Minibibliothek mit diesem letzten hinzugefügten Teil. Sie werden feststellen, dass das Beobachter-Array jetzt Objekte enthält, die es ermöglichen, die Abhängigkeiten jedes Beobachters beizubehalten.
class State { constructor(initialState = {}) { this.state = initialState; this.observers = []; return new Proxy(this, { get: (target, prop) => { if (prop in target.state) { return target.state[prop]; } return target[prop]; }, set: (target, prop, value) => { if (prop in target.state) { if (target.state[prop] !== value) { target.state[prop] = value; this.observers.forEach(({ observer, dependencies }) => { if (dependencies.has(prop)) { observer(this.state); } }); } } else { target[prop] = value; } }, }); } observe(observer) { const dependencies = new Set(); const proxy = new Proxy(this.state, { get: (target, prop) => { dependencies.add(prop); return target[prop]; }, }); observer(proxy); this.observers.push({ observer, dependencies }); } } const state = new State({ count: 0, text: '', }); state.observe(({ count }) => { console.log('Count changed', count); }); state.observe(({ text }) => { console.log('Text changed', text); }); state.observe((state) => { console.log('Count or text changed', state.count, state.text); }); state.count = 1; state.text = 'Hello, world!'; state.count = 1; // Output: // Count changed 0 // Text changed // Count or text changed 0 // Count changed 1 // Count or text changed 1 // Text changed Hello, world! // Count or text changed 1 Hello, world! // Count changed 2 // Count or text changed 2 Hello, world!
Und da haben Sie es: In 45 Codezeilen haben wir eine Mini-Statusverwaltungsbibliothek in JavaScript implementiert.
Wenn wir noch weiter gehen wollten, könnten wir Typvorschläge mit JSDoc hinzufügen oder diesen in TypeScript umschreiben, um Vorschläge zu Eigenschaften der Zustandsinstanz zu erhalten.
Wir könnten auch eine Unobserve-Methode hinzufügen, die für ein von State.observe zurückgegebenes Objekt verfügbar gemacht wird.
Es könnte auch nützlich sein, das Setter-Verhalten in eine setState-Methode zu abstrahieren, die es uns ermöglicht, mehrere Eigenschaften gleichzeitig zu ändern. Derzeit müssen wir jede Eigenschaft unseres Staates einzeln ändern, was mehrere Beobachter auslösen kann, wenn einige von ihnen Abhängigkeiten teilen.
Auf jeden Fall hoffe ich, dass Ihnen diese kleine Übung genauso viel Spaß gemacht hat wie mir und dass Sie dadurch etwas tiefer in das Konzept des Proxys in JavaScript eintauchen konnten.
Haftungsausschluss: Alle bereitgestellten Ressourcen stammen teilweise aus dem Internet. Wenn eine Verletzung Ihres Urheberrechts oder anderer Rechte und Interessen vorliegt, erläutern Sie bitte die detaillierten Gründe und legen Sie einen Nachweis des Urheberrechts oder Ihrer Rechte und Interessen vor und senden Sie ihn dann an die E-Mail-Adresse: [email protected] Wir werden die Angelegenheit so schnell wie möglich für Sie erledigen.
Copyright© 2022 湘ICP备2022001581号-3