Hagamos los peores Vue.js del mundo

Hace un tiempo publiqué un artículo similar sobre React donde, con un par de líneas de código, creamos un pequeño clon de React.js desde cero. Pero React está lejos de ser la única herramienta en el mundo front-end moderno, Vue.js está ganando popularidad rápidamente. Echemos un vistazo a cómo funciona este marco y creemos un clon primitivo similar a Vue.js con fines educativos.



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 .



All Articles