Heim  >  Artikel  >  Web-Frontend  >  Schreiben einer Zustandsverwaltungsbibliothek in JavaScript-Zeilen

Schreiben einer Zustandsverwaltungsbibliothek in JavaScript-Zeilen

王林
王林Original
2024-08-24 11:05:04848Durchsuche

Writing a state management library in lines of JavaScript

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.

API-Definition

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

Implementierung der State-Klasse

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 Grundstücken kompatibel. Ohne ins Detail zu gehen und um diese Implementierung zu vereinfachen, werden wir vorerst öffentliche Eigenschaften verwenden.

Lesen Sie Daten aus dem Statusobjekt mit einem Proxy

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 die Daten manipuliert.

Hinzufügen eines Setters zum Ändern der Statuswerte

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.

Beobachter implementieren

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?

Identifying Function Dependencies with Proxies

Once again, Proxies come to our rescue. To identify the dependencies of our observer functions, we can create a proxy of our state object, run them with it as an argument, and note which properties they accessed.

Simple, but effective.

When calling observers, all we have to do is check if they have a dependency on the updated property and trigger them only if so.

Here is the final implementation of our mini-library with this last part added. You will notice that the observers array now contains objects allowing to keep the dependencies of each observer.

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!

And there you have it, in 45 lines of code we have implemented a mini state management library in JavaScript.

Going further

If we wanted to go further, we could add type suggestions with JSDoc or rewrite this one in TypeScript to get suggestions on properties of the state instance.

We could also add an unobserve method that would be exposed on an object returned by State.observe.

It might also be useful to abstract the setter behavior into a setState method that allows us to modify multiple properties at once. Currently, we have to modify each property of our state one by one, which may trigger multiple observers if some of them share dependencies.

In any case, I hope that you enjoyed this little exercise as much as I did and that it allowed you to delve a little deeper into the concept of Proxy in JavaScript.

Das obige ist der detaillierte Inhalt vonSchreiben einer Zustandsverwaltungsbibliothek in JavaScript-Zeilen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Vorheriger Artikel:Parallele JavaScript-MaschineNächster Artikel:Parallele JavaScript-Maschine