Cómo hacer que el almacenamiento local sea reactivo en Vue

Índice
  1. Reactividad en Vue
  2. localStorageFunciones primordiales
  3. Cómo Vue recopila dependencias
  4. Seguimiento de quién llamó a localStorage
  5. El final de nuestro experimento mental

La reactividad es una de las mayores características de Vue. También es uno de los más misteriosos si no sabes lo que hace detrás de escena. ¿Por qué funciona con objetos y matrices y no con otras cosas como localStorage?

Respondamos esa pregunta y, mientras estamos en ello, hagamos que la reactividad de Vue funcione con localStorage.

Si tuviéramos que ejecutar el siguiente código, veríamos que el contador se muestra como un valor estático y no cambia como podríamos esperar debido al intervalo que cambia el valor en localStorage.

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

Si bien la counterpropiedad dentro de la instancia de Vue es reactiva, no cambiará solo porque cambiamos su origen en localStorage.

Hay varias soluciones para esto, quizás la mejor sea usar Vuex y mantener el valor de la tienda sincronizado con localStorage. Pero ¿qué pasa si necesitamos algo simple como lo que tenemos en este ejemplo? Tenemos que profundizar en cómo funciona el sistema de reactividad de Vue.

Reactividad en Vue

Cuando Vue inicializa una instancia de componente, observa la dataopción . Esto significa que recorre todas las propiedades de los datos y las convierte en captadores/definidores usando Object.defineProperty. Al tener un configurador personalizado para cada propiedad, Vue sabe cuándo cambia una propiedad y puede notificar a los dependientes que deben reaccionar al cambio. ¿Cómo sabe qué dependientes dependen de una propiedad? Al aprovechar los captadores, puede registrar cuándo una propiedad calculada, una función de vigilancia o una función de representación accede a una propiedad de datos.

// core/instance/state.jsfunction initData () {  // ...  observe(data)}
// core/observer/index.jsexport 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()    }  })}

Entonces, ¿por qué no es localStoragereactivo? Porque no es un objeto con propiedades.

Pero espera. Tampoco podemos definir captadores y definidores con matrices, pero las matrices en Vue siguen siendo reactivas. Esto se debe a que las matrices son un caso especial en Vue. Para tener matrices reactivas, Vue anula los métodos de matriz detrás de escena y los parchea junto con el sistema de reactividad de Vue.

¿Podríamos hacer algo similar con localStorage?

localStorageFunciones primordiales

Como primer intento, podemos arreglar nuestro ejemplo inicial anulando los métodos localStorage para realizar un seguimiento de qué instancias de componentes solicitaron un localStorageelemento.

// A map between localStorage item keys and a list of Vue instances that depend on itconst 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: `div    divCounter: {{ counter }}/div    divCounter is {{ even ? 'even' : 'odd' }}/div  /div`});
setInterval(() = {  const counter = localStorage.getItem("counter");  localStorage.setItem("counter", +counter + 1);}, 1000);

En este ejemplo, redefinimos getItemy setItempara recopilar y notificar los componentes que dependen de localStoragelos elementos. En el nuevo getItem, anotamos qué componente solicita qué artículo, y en setItems, nos comunicamos con todos los componentes que solicitaron el artículo y reescribimos sus datos.

Para que el código anterior funcione, debemos pasar una referencia a la instancia del componente getItemy eso cambia su firma de función. Tampoco podemos usar más la función de flecha porque de lo contrario no tendríamos el thisvalor correcto.

Si queremos hacerlo mejor, tenemos que profundizar más. Por ejemplo, ¿cómo podríamos realizar un seguimiento de las personas dependientes sin transmitirlas explícitamente ?

Cómo Vue recopila dependencias

Para inspirarnos, podemos volver al sistema de reactividad de Vue. Anteriormente vimos que el captador de una propiedad de datos suscribirá a la persona que llama a los cambios adicionales de la propiedad cuando se acceda a la propiedad de datos. ¿Pero cómo sabe quién hizo la llamada? Cuando obtenemos un dataaccesorio, su función de obtención no tiene ninguna información sobre quién fue la persona que llamó. Las funciones getter no tienen entradas . ¿Cómo sabe a quién registrar como dependiente?

Cada propiedad de datos mantiene una lista de sus dependientes que necesitan reaccionar en una clase Dep . Si profundizamos en esta clase, podemos ver que el dependiente en sí ya está definido en una variable de destino estática cada vez que se registra . Este objetivo lo establece una clase Vigilante hasta ahora misteriosa . De hecho, cuando una propiedad de datos cambia, estos observadores serán notificados e iniciarán la nueva representación del componente o el nuevo cálculo de una propiedad calculada.

Pero, de nuevo, ¿quiénes son?

Cuando Vue hace que la dataopción sea observable, también crea observadores para cada función de propiedad calculada , así como todas las funciones de vigilancia (que no deben confundirse con la clase Watcher) y la función de representación de cada instancia de componente . Los observadores son como compañeros para estas funciones. Principalmente hacen dos cosas:

  1. Evalúan la función cuando se crean. Esto desencadena la colección de dependencias.
  2. Vuelven a ejecutar su función cuando se les notifica que un valor en el que confían ha cambiado. En última instancia, esto volverá a calcular una propiedad calculada o volverá a representar un componente completo.

Hay un paso importante que ocurre antes de que los observadores llamen a la función de la que son responsables: se establecen como objetivo en una variable estática en la clase Dep. Esto garantiza que estén registrados como dependientes cuando se accede a una propiedad de datos reactiva.

Seguimiento de quién llamó a localStorage

No podemos hacer eso exactamente porque no tenemos acceso a la mecánica interna de Vue. Sin embargo, podemos usar la idea de Vue que permite a un observador establecer el objetivo en una propiedad estática antes de llamar a la función de la que es responsable. ¿Podríamos establecer una referencia a la instancia del componente antes de que localStoragese llame?

Si asumimos que localStoragese llama mientras se configura la opción de datos, entonces podemos conectarnos a beforeCreatey created. Estos dos enlaces se activan antes y después de inicializar la dataopción, por lo que podemos establecer y luego borrar una variable de destino con una referencia a la instancia del componente actual (a la que tenemos acceso en los enlaces del ciclo de vida). Luego, en nuestros captadores personalizados, podemos registrar este objetivo como dependiente.

Lo último que tenemos que hacer es hacer que estos ganchos del ciclo de vida formen parte de todos nuestros componentes. Podemos hacerlo con un mixin global para todo el proyecto.

// A map between localStorage item keys and a list of Vue instances that depend on itconst storeItemSubscribers = {};// The Vue instance that is currently being initialisedlet 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;  }});

Ahora, cuando ejecutemos nuestro ejemplo inicial, obtendremos un contador que aumenta el número cada segundo.

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

El final de nuestro experimento mental

Si bien resolvimos nuestro problema inicial, tenga en cuenta que esto es principalmente un experimento mental. Carece de varias funciones, como el manejo de elementos eliminados y instancias de componentes desmontados. También viene con restricciones, como que el nombre de propiedad de la instancia del componente requiere el mismo nombre que el elemento almacenado en localStorage. Dicho esto, el objetivo principal es tener una mejor idea de cómo funciona la reactividad de Vue entre bastidores y aprovecharla al máximo, así que eso es lo que espero que obtengan de todo esto.

SUSCRÍBETE A NUESTRO BOLETÍN 
No te pierdas de nuestro contenido ni de ninguna de nuestras guías para que puedas avanzar en los juegos que más te gustan.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Subir

Este sitio web utiliza cookies para mejorar tu experiencia mientras navegas por él. Este sitio web utiliza cookies para mejorar tu experiencia de usuario. Al continuar navegando, aceptas su uso. Mas informacion