Implementación básica de Vue.js con Object.defineProperty y patrón Observer

El mecanismo central de Vue.js se basa en la reactividad de datos y la actualización automática del DOM. A continuación se describe una implementación simplificada llamada MyVue, que replica los principios fundamentales del framework original.

  1. Principios básicos

La arquitectura MVVM (Model-View-ViewModel) permite la sincronización bidireccional entre el modelo de datos y la vista. Esto se logra mediante:

  • Interceptación de cambios en propiedades del objeto usando Object.defineProperty.
  • Patrón Observer (suscriptor-publicador) para notificar actualizaciones a las dependencias.
  • Compilación del DOM para identifciar expresiones de enlace como {{prop}} o directivas como v-model.
  1. Interceptación de propiedades con Object.defineProperty

Esta API nativa de JavaScript permite definir comportamientos personalizados al leer (get) o escribir (set) propiedades de un objeto:

const reactiveObj = {};
Object.defineProperty(reactiveObj, 'message', {
  get() {
    console.log('Leyendo valor');
    return this._message;
  },
  set(newValue) {
    console.log('Actualizando valor:', newValue);
    this._message = newValue;
  }
});
  1. Compilación del DOM

Se recorre el árbol DOM para detectar enlaces de datos. Se utilizan fragmentos de documento para manipulación eficiente:

function createFragment(rootNode, context) {
  const fragment = document.createDocumentFragment();
  let child;
  while ((child = rootNode.firstChild)) {
    processNode(child, context);
    fragment.appendChild(child);
    if (child.childNodes.length) {
      createFragment(child, context);
    }
  }
  return fragment;
}
  1. Sistema de reactividad

Cada propiedad reactiva mantiene una lista de observadores (watchers) que deben actualizarse cuando cambia su valor:

class Dependency {
  constructor() {
    this.watchers = [];
  }
  add(watcher) {
    this.watchers.push(watcher);
  }
  notify() {
    this.watchers.forEach(w => w.update());
  }
}

function makeReactive(obj, key, value) {
  const dep = new Dependency();
  Object.defineProperty(obj, key, {
    get() {
      if (Dependency.currentWatcher) {
        dep.add(Dependency.currentWatcher);
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      value = newValue;
      dep.notify();
    }
  });
}
  1. Observadores (Watchers)

Los watchers conectan las propiedades reactivas con nodos específicos del DOM:

class Watcher {
  constructor(vm, node, property) {
    Dependency.currentWatcher = this;
    this.vm = vm;
    this.node = node;
    this.property = property;
    this.update();
    Dependency.currentWatcher = null;
  }
  
  update() {
    this.value = this.vm[this.property];
    if (this.node.nodeType === 1) {
      this.node.value = this.value;
    } else if (this.node.nodeType === 3) {
      this.node.textContent = this.value;
    }
  }
}
  1. Implementación completa

Integrando todos los componentes en una clase principle:

class MyVue {
  constructor(options) {
    this.$data = options.data;
    this.observe(this.$data);
    
    const root = document.querySelector(options.el);
    const fragment = this.createFragment(root.cloneNode(true));
    root.innerHTML = '';
    root.appendChild(fragment);
  }
  
  observe(obj) {
    Object.keys(obj).forEach(key => {
      makeReactive(this, key, obj[key]);
    });
  }
  
  createFragment(node) {
    if (node.nodeType === 1) {
      const attrs = [...node.attributes];
      attrs.forEach(attr => {
        if (attr.name === 'v-model') {
          const prop = attr.value;
          node.value = this[prop];
          node.addEventListener('input', e => {
            this[prop] = e.target.value;
          });
          new Watcher(this, node, prop);
        }
      });
    } else if (node.nodeType === 3) {
      const match = node.textContent.match(/\{\{(.+?)\}\}/);
      if (match) {
        const prop = match[1].trim();
        node.textContent = this[prop];
        new Watcher(this, node, prop);
      }
    }
    
    node.childNodes.forEach(child => {
      this.createFragment(child);
    });
    
    return node;
  }
}
  1. Flujo de funcionamiento

  1. Inicialización: Las propiedades del modelo se convierten en reactivas mediante Object.defineProperty.
  2. Compilación: El DOM se analiza para identificar enlaces de datos y crear watchers correspondientes.
  3. Dependencias: Durante la creación de watchers, se establecen relaciones entre propiedades y nodos del DOM.
  4. Actualización: Al modificar una propiedad, se notifica a todos sus watchers para actualizar la vista.

Etiquetas: vue.js Object.defineProperty Observer Pattern Reactivity MVVM

Publicado el 8-25 12:04