Cómo hacer que el almacenamiento local sea reactivo en Vue
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 counter
propiedad 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 data
opció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 localStorage
reactivo? 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 localStorage
elemento.
// 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 getItem
y setItem
para recopilar y notificar los componentes que dependen de localStorage
los 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 getItem
y 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 this
valor 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 data
accesorio, 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 data
opció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:
- Evalúan la función cuando se crean. Esto desencadena la colección de dependencias.
- 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 localStorage
se llame?
Si asumimos que localStorage
se llama mientras se configura la opción de datos, entonces podemos conectarnos a beforeCreate
y created
. Estos dos enlaces se activan antes y después de inicializar la data
opció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.
Deja un comentario