Catégories
Astuces et Design

Comment rendre localStorage réactif dans Vue

Réactivité est l'une des meilleures fonctionnalités de Vue. C'est aussi l'un des plus mystérieux si vous ne savez pas ce qu'il fait en coulisses. Comme, pourquoi ça marche avec des objets et des tableaux et pas avec d'autres choses, comme localStorage?

Répondons à cette question, et pendant que nous y sommes, faire Vue réactivité travailler avec localStorage.

Si nous devions exécuter le code suivant, nous verrions que le compteur est affiché comme une valeur statique et ne change pas comme nous pouvons nous y attendre en raison de l'intervalle modifiant la valeur dans localStorage.

new Vue({
  el: "#counter",
  data: () => ({
    counter: localStorage.getItem("counter")
  }),
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `
   
Counter: {{ counter }}
   
Counter is {{ even ? 'even' : 'odd' }}
 
` });
// some-other-file.js
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

Tandis que le counter propriété à l'intérieur de l'instance Vue est réactif, il ne changera pas simplement parce que nous avons changé son origine localStorage.

Il existe plusieurs solutions pour cela, la plus agréable est peut-être d'utiliser Vuex et de synchroniser la valeur du magasin avec localStorage. Mais que faire si nous avons besoin de quelque chose de simple comme ce que nous avons dans cet exemple? Nous devons plonger dans le fonctionnement du système de réactivité de Vue.

Réactivité dans Vue

Lorsque Vue initialise une instance de composant, il observe la data option. Cela signifie qu'il parcourt toutes les propriétés des données et les convertit en getters / setters à l'aide de Object.defineProperty. En ayant un setter personnalisé pour chaque propriété, Vue sait quand une propriété change et il peut notifier les dépendants qui doivent réagir au changement. Comment sait-il quelles personnes à charge dépendent d'une propriété? En appuyant sur les getters, il peut s'enregistrer lorsqu'une propriété calculée, une fonction de surveillance ou une fonction de rendu accède à un accessoire de données.

// core/instance/state.js
function initData () {
  // ...
  observe(data)
}
// core/observer/index.js
export function observe (value) {
  // ...
  new Observer(value)
  // ...
}

export class Observer {
  // ...
  constructor (value) {
    // ...
    this.walk(value)
  }
  
  walk (obj) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys(i))
    }
  }
} 


export function defineReactive (obj, key, ...) {
  const dep = new Dep()
  // ...
  Object.defineProperty(obj, key, {
    // ...
    get() {
      // ...
      dep.depend()
      // ...
    },
    set(newVal) {
      // ...
      dep.notify()
    }
  })
}

Alors, pourquoi n'est-ce pas localStorage réactif? Parce que ce n'est pas un objet avec des propriétés.

Mais attendez. Nous ne pouvons pas non plus définir les getters et setters avec des tableaux, mais les tableaux dans Vue sont toujours réactifs. C'est parce que les tableaux sont un cas spécial dans Vue. Pour avoir des tableaux réactifs, Vue remplace les méthodes de tableau en arrière-plan et les corrige avec le système de réactivité de Vue.

Pouvons-nous faire quelque chose de similaire avec localStorage?

Primordial localStorage les fonctions

Dans un premier temps, nous pouvons corriger notre exemple initial en remplaçant les méthodes localStorage pour garder une trace des instances de composants qui ont demandé un localStorage article.

// A map between localStorage item keys and a list of Vue instances that depend on it
const storeItemSubscribers = {};


const getItem = window.localStorage.getItem;
localStorage.getItem = (key, target) => {
  console.info("Getting", key);


  // Collect dependent Vue instance
  if (!storeItemSubscribers(key)) storeItemSubscribers(key) = ();
  if (target) storeItemSubscribers(key).push(target);


  // Call the original function 
  return getItem.call(localStorage, key);
};


const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
  console.info("Setting", key, value);


  // Update the value in the dependent Vue instances
  if (storeItemSubscribers(key)) {
    storeItemSubscribers(key).forEach((dep) => {
      if (dep.hasOwnProperty(key)) dep(key) = value;
    });
  }


  // Call the original function
  setItem.call(localStorage, key, value);
};
new Vue({
  el: "#counter",
  data: function() {
    return {
      counter: localStorage.getItem("counter", this) // We need to pass 'this' for now
    }
  },
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `
   
Counter: {{ counter }}
   
Counter is {{ even ? 'even' : 'odd' }}
 
` });
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

Dans cet exemple, nous redéfinissons getItem et setItem afin de collecter et de notifier les composants qui dépendent de localStorage articles. Dans le nouveau getItem, nous notons quel composant demande quel article, et dans setItems, nous contactons tous les composants qui ont demandé l'article et réécrivons leurs données.

Afin de faire fonctionner le code ci-dessus, nous devons transmettre une référence à l'instance du composant pour getItem et cela change sa signature de fonction. Nous ne pouvons pas non plus utiliser la fonction flèche car nous n'aurions pas autrement la bonne this valeur.

Si nous voulons faire mieux, nous devons creuser plus profondément. Par exemple, comment pourrions-nous suivre les personnes à charge sans explicitement les transmettre?

Comment Vue collecte les dépendances

Pour l'inspiration, nous pouvons revenir au système de réactivité de Vue. Nous avons vu précédemment que le getter d'une propriété de données abonnerait l'appelant aux modifications supplémentaires de la propriété lors de l'accès à la propriété de données. Mais comment sait-il qui a appelé? Quand nous obtenons un data prop, sa fonction getter n'a aucune entrée concernant l'identité de l'appelant. Les fonctions Getter n'ont aucune entrée. Comment sait-il qui s'inscrire comme personne à charge?

Chaque propriété de données conserve une liste de ses dépendants qui doivent réagir dans une classe Dep. Si nous approfondissons cette classe, nous pouvons voir que la dépendance elle-même est déjà définie dans une variable cible statique chaque fois qu'elle est enregistrée. Cet objectif est fixé par une classe de Watcher jusqu'ici mystérieuse. En fait, lorsqu'une propriété de données change, ces observateurs seront effectivement avertis et ils initieront le nouveau rendu du composant ou le recalcul d'une propriété calculée.

Mais, encore une fois, qui sont-ils?

Quand Vue fait le data observable, elle crée également des observateurs pour chaque fonction de propriété calculée, ainsi que toutes les fonctions d’observation (qui ne doivent pas être confondues avec la classe Watcher), et la fonction de rendu de chaque instance de composant. Les observateurs sont comme des compagnons pour ces fonctions. Ils font principalement deux choses:

  1. Ils évaluent la fonction lors de leur création. Cela déclenche la collecte de dépendances.
  2. Ils réexécutent leur fonction lorsqu'ils sont informés qu'une valeur sur laquelle ils s'appuient a changé. Cela recalculera finalement une propriété calculée ou restituera un composant entier.

Il y a une étape importante qui se produit avant que les observateurs appellent la fonction dont ils sont responsables: ils se mettre comme cible dans une variable statique de la classe Dep. Cela garantit qu'ils sont enregistrés comme dépendants lors de l'accès à une propriété de données réactive.

Garder une trace de qui a appelé localStorage

Nous ne pouvons pas exactement faites cela parce que nous n'avons pas accès à la mécanique interne de Vue. Cependant, nous pouvons utiliser le idée de Vue qui permet à un observateur de définir la cible dans une propriété statique avant d'appeler la fonction dont il est responsable. Pourrions-nous définir une référence à l'instance de composant avant localStorage se fait appeler?

Si nous supposons que localStorage est appelé lors de la définition de l'option de données, nous pouvons alors nous connecter à beforeCreate et created. Ces deux crochets sont déclenchés avant et après l'initialisation du data , afin que nous puissions définir, puis effacer, une variable cible avec une référence à l'instance de composant actuelle (à laquelle nous avons accès dans les hooks du cycle de vie). Ensuite, dans nos getters personnalisés, nous pouvons enregistrer cette cible en tant que personne à charge.

La dernière chose que nous devons faire est d'intégrer ces crochets de cycle de vie à tous nos composants. Nous pouvons le faire avec un mixage global pour l'ensemble du projet.

// A map between localStorage item keys and a list of Vue instances that depend on it
const storeItemSubscribers = {};

// The Vue instance that is currently being initialised
let target = undefined;

const getItem = window.localStorage.getItem;
localStorage.getItem = (key) => {
  console.info("Getting", key);

  // Collect dependent Vue instance
  if (!storeItemSubscribers(key)) storeItemSubscribers(key) = ();
  if (target) storeItemSubscribers(key).push(target);

  // Call the original function
  return getItem.call(localStorage, key);
};

const setItem = window.localStorage.setItem;
localStorage.setItem = (key, value) => {
  console.info("Setting", key, value);

  // Update the value in the dependent Vue instances
  if (storeItemSubscribers(key)) {
    storeItemSubscribers(key).forEach((dep) => {
      if (dep.hasOwnProperty(key)) dep(key) = value;
    });
  }
  
  // Call the original function
  setItem.call(localStorage, key, value);
};

Vue.mixin({
  beforeCreate() {
    console.log("beforeCreate", this._uid);
    target = this;
  },
  created() {
    console.log("created", this._uid);
    target = undefined;
  }
});

Maintenant, lorsque nous exécutons notre exemple initial, nous obtiendrons un compteur qui augmente le nombre à chaque seconde.

new Vue({
  el: "#counter",
  data: () => ({
    counter: localStorage.getItem("counter")
  }),
  computed: {
    even() {
      return this.counter % 2 == 0;
    }
  },
  template: `
   
Counter: {{ counter }}
   
Counter is {{ even ? 'even' : 'odd' }}
 
` });
setInterval(() => {
  const counter = localStorage.getItem("counter");
  localStorage.setItem("counter", +counter + 1);
}, 1000);

La fin de notre expérience de pensée

Bien que nous ayons résolu notre problème initial, gardez à l'esprit qu'il s'agit principalement d'une expérience de pensée. Il manque plusieurs fonctionnalités, comme la gestion des éléments supprimés et des instances de composants non montés. Il est également livré avec des restrictions, comme le nom de propriété de l'instance de composant nécessite le même nom que l'élément stocké dans localStorage. Cela dit, l'objectif principal est d'avoir une meilleure idée du fonctionnement de la réactivité de Vue dans les coulisses et d'en tirer le meilleur parti, c'est donc ce que j'espère que vous tirerez de tout cela.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *