Reactividad
Al igual que React.js, Vue es reactivo, lo que significa que todos los cambios en el estado de la aplicación se reflejan automáticamente en el DOM. Pero a diferencia de React, Vue realiza un seguimiento de las dependencias en el tiempo de renderizado y solo actualiza las partes relacionadas sin ninguna "comparación".
La clave de la reactividad de Vue.js es el método
Object.defineProperty
. Le permite especificar un método getter / setter personalizado en un campo de objeto e interceptar cada acceso a él:
const obj = {a: 1};
Object.defineProperty(obj, 'a', {
get() { return 42; },
set(val) { console.log('you want to set "a" to', val); }
});
console.log(obj.a); // prints '42'
obj.a = 100; // prints 'you want to set "a" to 100'
Con esto, podemos determinar cuándo se está accediendo a una propiedad en particular, o cuándo cambia, y luego reevaluar todas las expresiones dependientes después de que la propiedad haya cambiado.
Expresiones
Vue.js le permite vincular una expresión de JavaScript a un atributo de nodo DOM mediante una directiva. Por ejemplo,
<div v-text="s.toUpperCase()"></div>
establecerá el texto dentro del div en un valor de variable en mayúsculas
s
.
El enfoque más simple para evaluar cadenas, como
s.toUpperCase()
, es usar
eval()
. Aunque eval nunca se consideró una solución segura, podemos intentar mejorarla un poco envolviéndola en una función y pasándola en un contexto global personalizado:
const call = (expr, ctx) =>
new Function(`with(this){${`return ${expr}`}}`).bind(ctx)();
call('2+3', null); // returns 5
call('a+1', {a:42}); // returns 43
call('s.toUpperCase()', {s:'hello'}); // returns "HELLO"
Esto es un poco más seguro que el nativo
eval
y es suficiente para el marco simple que estamos construyendo.
Apoderado
Ahora podemos usar
Object.defineProperty
para envolver cada propiedad del objeto de datos; se puede utilizar
call()
para evaluar expresiones arbitrarias y para saber a qué propiedades accedió la expresión directa o indirectamente. También necesitamos poder determinar cuándo se debe reevaluar la expresión porque una de sus variables ha cambiado:
const data = {a: 1, b: 2, c: 3, d: 'foo'}; // Data model
const vars = {}; // List of variables used by expression
// Wrap data fields into a proxy that monitors all access
for (const name in data) {
let prop = data[name];
Object.defineProperty(data, name, {
get() {
vars[name] = true; // variable has been accessed
return prop;
},
set(val) {
prop = val;
if (vars[name]) {
console.log('Re-evaluate:', name, 'changed');
}
}
});
}
// Call our expression
call('(a+c)*2', data);
console.log(vars); // {"a": true, "c": true} -- these two variables have been accessed
data.a = 5; // Prints "Re-evaluate: a changed"
data.b = 7; // Prints nothing, this variable does not affect the expression
data.c = 11; // Prints "Re-evaluate: c changed"
data.d = 13; // Prints nothing.
Directivas
Ahora podemos evaluar expresiones arbitrarias y realizar un seguimiento de qué expresiones evaluar cuando cambia una variable de datos en particular. Todo lo que queda es asignar expresiones a ciertas propiedades del nodo DOM y cambiarlas cuando cambian los datos.
Al igual que en Vue.js, usaremos atributos especiales como
q-on:click
vincular controladores de eventos,
q-text
vincular textContent,
q-bind:style
vincular estilo CSS, etc. Utilizo el prefijo "q-" aquí porque "q" es similar a "vue".
Aquí hay una lista parcial de posibles directivas compatibles:
const directives = {
// Bind innerText to an expression value
text: (el, _, val, ctx) => (el.innerText = call(val, ctx)),
// Bind event listener
on: (el, name, val, ctx) => (el[`on${name}`] = () => call(val, ctx)),
// Bind node attribute to an expression value
bind: (el, name, value, ctx) => el.setAttribute(name, call(value, ctx)),
};
Cada directiva es una función que toma un nodo DOM, un nombre de parámetro opcional para casos como
q-on:click
(el nombre será "clic"). También requiere una cadena de expresión (
value
) y un objeto de datos para usar como contexto de expresión.
Ahora que tenemos todos los componentes básicos, ¡es hora de unir todo!
Resultado final
const call = .... // Our "safe" expression evaluator
const directives = .... // Our supported directives
// Currently evaluated directive, proxy uses it as a dependency
// of the individual variables accessed during directive evaluation
let $dep;
// A function to iterate over DOM node and its child nodes, scanning all
// attributes and binding them as directives if needed
const walk = (node, q) => {
// Iterate node attributes
for (const {name, value} of node.attributes) {
if (name.startsWith('q-')) {
const [directive, event] = name.substring(2).split(':');
const d = directives[directive];
// Set $dep to re-evaluate this directive
$dep = () => d(node, event, value, q);
// Evaluate directive for the first time
$dep();
// And clear $dep after we are done
$dep = undefined;
}
}
// Walk through child nodes
for (const child of node.children) {
walk(child, q);
}
};
// Proxy uses Object.defineProperty to intercept access to
// all `q` data object properties.
const proxy = q => {
const deps = {}; // Dependent directives of the given data object
for (const name in q) {
deps[name] = []; // Dependent directives of the given property
let prop = q[name];
Object.defineProperty(q, name, {
get() {
if ($dep) {
// Property has been accessed.
// Add current directive to the dependency list.
deps[name].push($dep);
}
return prop;
},
set(value) { prop = value; },
});
}
return q;
};
// Main entry point: apply data object "q" to the DOM tree at root "el".
const Q = (el, q) => walk(el, proxy(q));
Un marco reactivo similar a Vue.js en su máxima expresión. ¿Qué tan útil es? He aquí un ejemplo:
<div id="counter">
<button q-on:click="clicks++">Click me</button>
<button q-on:click="clicks=0">Reset</button>
<p q-text="`Clicked ${clicks} times`"></p>
</div>
Q(counter, {clicks: 0});
Al presionar un botón, se incrementa el contador y se actualiza automáticamente el contenido
<p>
. Al hacer clic en otro, el contador se pone a cero y también se actualiza el texto.
Como puede ver, Vue.js parece mágico a primera vista, pero por dentro es muy simple y la funcionalidad básica se puede implementar en solo unas pocas líneas de código.
Pasos adicionales
Si está interesado en aprender más sobre Vue.js, intente implementar "q-if" para alternar la visibilidad de los elementos según una expresión, o "q-each" para vincular listas de elementos secundarios duplicados (este sería un buen ejercicio ).
La fuente completa del nanoframework Q está en Github . ¡No dude en donar si detecta un problema o desea sugerir una mejora!
En conclusión, debo mencionar que
Object.defineProperty
se usó en el Vue 2 Vue 3 y los creadores han cambiado a otra instalación proporcionada por ES6, a saber
Proxy
y
Reflect
... Proxy le permite pasar un controlador para interceptar el acceso a las propiedades del objeto, como en nuestro ejemplo, mientras que Reflect le permite acceder a las propiedades del objeto desde el proxy y mantener el
this
objeto intacto (a diferencia de nuestro ejemplo con defineProperty).
Dejo tanto Proxy / Reflect como un ejercicio para el lector, así que quien haga una solicitud para usarlos correctamente en Q , estaré feliz de combinar eso. ¡Buena suerte!
Espero que hayas disfrutado del artículo. Puede seguir las noticias y compartir sugerencias en Github , Twitter o suscribirse a través de rss .