Maison  >  Article  >  interface Web  >  Écrire une bibliothèque de gestion d'état en lignes JavaScript

Écrire une bibliothèque de gestion d'état en lignes JavaScript

王林
王林original
2024-08-24 11:05:04851parcourir

Writing a state management library in lines of JavaScript

La gestion de l'état est l'une des parties les plus importantes d'une application Web. De l'utilisation de variables globales aux hooks React en passant par l'utilisation de librairies tierces comme MobX, Redux ou XState pour ne citer que ces 3, c'est l'un des sujets qui alimente le plus de discussions tant il est important de le maîtriser pour concevoir un application fiable et efficace.

Aujourd'hui, je propose de construire une mini bibliothèque de gestion d'états en moins de 50 lignes de JavaScript basée sur la notion d'observables. Celui-ci peut certes être utilisé tel quel pour des petits projets, mais au-delà de cet exercice pédagogique je vous recommande tout de même de vous tourner vers des solutions plus standardisées pour vos vrais projets.

Définition de l'API

Lors du démarrage d'un nouveau projet de bibliothèque, il est important de définir dès le départ à quoi pourrait ressembler son API afin de figer son concept et d'orienter son développement avant même de penser aux détails techniques de mise en œuvre. Pour un vrai projet, il est même possible de commencer à écrire des tests à ce moment pour valider l'implémentation de la librairie telle qu'elle est écrite selon une approche TDD.

Ici, nous souhaitons exporter une seule classe que nous appellerons State qui sera instanciée avec un objet contenant l'état initial et une seule méthode d'observation qui nous permet de souscrire aux changements d'état avec des observateurs. Ces observateurs ne doivent être exécutés que si l'une de leurs dépendances a changé.

Pour changer l'état, nous souhaitons utiliser directement les propriétés de la classe plutôt que de passer par une méthode comme setState.

Parce qu'un extrait de code vaut mille mots, voici à quoi pourrait ressembler notre implémentation finale en cours d'utilisation :

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

Implémentation de la classe State

Commençons par créer une classe State qui accepte un état initial dans son constructeur et expose une méthode d'observation que nous implémenterons plus tard.

class State {
  constructor(initialState = {}) {
    this.state = initialState;
    this.observers = [];
  }

  observe(observer) {
    this.observers.push(observer);
  }
}

Ici nous choisissons d'utiliser un objet d'état intermédiaire interne qui nous permettra de conserver les valeurs d'état. Nous stockons également les observateurs dans un tableau d'observateurs interne qui sera utile lorsque nous aurons terminé cette implémentation.

Comme ces 2 propriétés ne seront utilisées qu'à l'intérieur de cette classe, nous pourrions les déclarer comme privées avec un peu de sucre syntaxique en les préfixant d'un # et en ajoutant une déclaration initiale sur la classe :

class State {
  #state = {};
  #observers = [];

  constructor(initialState = {}) {
    this.#state = initialState;
    this.#observers = [];
  }

  observe(observer) {
    this.#observers.push(observer);
  }
}

En principe, ce serait une bonne pratique, mais nous utiliserons des proxys à l'étape suivante et ils ne sont pas compatibles avec les propriétés privées. Sans entrer dans les détails et pour faciliter cette mise en œuvre, nous utiliserons pour l'instant des propriétés publiques.

Lire les données de l'objet d'état avec un proxy

Lorsque nous avons présenté les spécifications de ce projet, nous souhaitions accéder aux valeurs d'état directement sur l'instance de classe et non comme une entrée dans son objet d'état interne.

Pour cela, nous utiliserons un objet proxy qui sera renvoyé lors de l'initialisation de la classe.

Comme son nom l'indique, un Proxy permet de créer un intermédiaire pour qu'un objet intercepte certaines opérations, notamment ses getters et setters. Dans notre cas, nous créons un Proxy exposant un premier getter qui nous permet d'exposer les entrées de l'objet state comme si elles appartenaient directement à l'instance State.

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

Nous pouvons désormais définir un objet d'état initial lors de l'instanciation de State, puis récupérer ses valeurs directement à partir de cette instance. Voyons maintenant comment manipuler ses données.

Ajout d'un setter pour modifier les valeurs d'état

Nous avons ajouté un getter, donc la prochaine étape logique est d'ajouter un setter nous permettant de manipuler l'objet d'état.

On vérifie d'abord que la clé appartient à cet objet, puis on vérifie que la valeur a bien changé pour éviter des mises à jour inutiles, et enfin on met à jour l'objet avec la nouvelle valeur.

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

Nous en avons maintenant terminé avec la partie lecture et écriture des données. Nous pouvons modifier la valeur de l'état, puis récupérer cette modification. Jusqu'à présent, notre implémentation n'est pas très utile, alors implémentons les observateurs maintenant.

Mise en œuvre des observateurs

Nous avons déjà un tableau contenant les fonctions d'observateurs déclarées sur notre instance, il ne nous reste donc plus qu'à les appeler une par une à chaque fois qu'une valeur a changé.

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, nous réagissons désormais aux changements de données !

Petit problème cependant. Si vous avez prêté attention jusqu'à présent, nous voulions à l'origine exécuter les observateurs uniquement si l'une de leurs dépendances changeait. Cependant, si nous exécutons ce code, nous voyons que chaque observateur s'exécute à chaque fois qu'une partie de l'état est modifiée.

Mais alors comment identifier les dépendances de ces fonctions ?

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.

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Article précédent:Machine JavaScript parallèleArticle suivant:Machine JavaScript parallèle