Principios de Enlace Bidireccional en Vue

En Vue.js, el enlace bidireccional entre la capa de vista y el modelo de datos permite una comunicación fluida. Cuando los datos en la capa de vista se transmiten al modelo, Vue utiliza Object.defineProperty para interceptar cada propiedad, asociando eventos de escucha. Al detectar cambios, el método set de defineProperty actualiza automáticamente la capa de vista.

Implementación con Object.defineProperty

El método central para lograr este comportamiento es Object.defineProperty. Para monitorear todas las propiedades de un objeto, podemos utilizar un enfoque recursivo que recorra cada propiedad y la procese con Object.defineProperty.

// Implementar un Observer para monitorear y interceptar todas las propiedades
function Observador(datos) {
    this.datos = datos;
    this.recorrer(datos);
}

Observador.prototype = {
    recorrer: function(datos) {
        var self = this;
        Object.keys(datos).forEach(function(clave) {
            self.definirReactividad(datos, clave, datos[clave]);
        });
    },
    definirReactividad: function(datos, clave, valor) {
        var dependencia = new Dependencia();
        var objetoHijo = observar(valor);
        Object.defineProperty(datos, clave, {
            enumerable: true,
            configurable: true,
            get: function() {
                if (Dependencia.objetivo) {
                    dependencia.agregarSuscriptor(Dependencia.objetivo);
                }
                return valor;
            },
            set: function(nuevoValor) {
                if (nuevoValor === valor) {
                    return;
                }
                valor = nuevoValor;
                dependencia.notificar();
            }
        });
    }
};

function observar(valor, contexto) {
    if (!valor || typeof valor !== 'object') {
        return;
    }
    return new Observador(valor);
};

function Dependencia() {
    this.suscriptores = [];
}
Dependencia.prototype = {
    agregarSuscriptor: function(suscriptor) {
        this.suscriptores.push(suscriptor);
    },
    notificar: function() {
        this.suscriptores.forEach(function(suscriptor) {
            suscriptor.actualizar();
        });
    }
};
Dependencia.objetivo = null;

Implementación del Watcher para monitoreo

Al inicializar un suscripter Watcher, podemos activar el método get correspondiente para realizar la suscripción. Al obtener el valor de la propiedad, se activa automáticamente la función get del monitor, gracias al uso de Object.defineProperty.

// Implementar un suscriptor Watcher que reaccione a cambios en propiedades
function ObservadorVue(vm, expresion, callback) {
    this.callback = callback;
    this.vm = vm;
    this.expresion = expresion;
    this.valor = this.obtener(); // Agrega este observador al sistema de dependencias
}

ObservadorVue.prototype = {
    actualizar: function() {
        this.ejecutar();
    },
    ejecutar: function() {
        var valor = this.vm.datos[this.expresion];
        var valorAntiguo = this.valor;
        if (valor !== valorAntiguo) {
            this.valor = valor;
            this.callback.call(this.vm, valor, valorAntiguo);
        }
    },
    obtener: function() {
        Dependencia.objetivo = this; // Almacena este observador
        var valor = this.vm.datos[this.expresion]; // Fuerza la ejecución del get
        Dependencia.objetivo = null; // Libera este observador
        return valor;
    }
};

Análisis y enlace de nodos DOM con Compile

El compilador Compile realiza los siguientes pasos:

  1. Analiza las directivas de la plantilla y reemplaza los datos, inicializando la vista
  2. Enlaza los nodos de la plantilla con las funciones de actualización correspondientes, inicializando los suscriptores apropiados

Para analizar la plantilla, primero obtenemos los elementos DOM y procesamos los nodos que contienen directivas. Para optimizar las operaciones DOM, creamos un fragmento que contiene los nodos a procesar:

// Implementar un compilador Compile que analice nodos y cree suscriptores
function Compilar(elemento, vm) {
    this.vm = vm;
    this.elemento = document.querySelector(elemento);
    this.fragmento = null;
    this.inicializar();
}

Compilar.prototype = {
    inicializar: function() {
        if (this.elemento) {
            this.fragmento = this.convertirAFragmento(this.elemento);
            this.compilarElemento(this.fragmento);
            this.elemento.appendChild(this.fragmento);
        } else {
            console.log('El elemento DOM no existe');
        }
    },
    convertirAFragmento: function(el) {
        var fragmento = document.createDocumentFragment();
        var hijo = el.firstChild;
        while (hijo) {
            fragmento.appendChild(hijo);
            hijo = el.firstChild;
        }
        return fragmento;
    },
    compilarElemento: function(el) {
        var nodosHijos = el.childNodes;
        var self = this;
        [].slice.call(nodosHijos).forEach(function(nodo) {
            var regex = /\{\{(.*)\}\}/;
            var texto = nodo.textContent;

            if (self.esNodoElemento(nodo)) {  
                self.compilarNodo(nodo);
            } else if (self.esNodoTexto(nodo) && regex.test(texto)) {
                self.compilarTexto(nodo, regex.exec(texto)[1]);
            }

            if (nodo.childNodes && nodo.childNodes.length) {
                self.compilarElemento(nodo);
            }
        });
    },
    compilarNodo: function(nodo) {
        var atributosNodo = nodo.attributes;
        var self = this;
        Array.prototype.forEach.call(atributosNodo, function(atributo) {
            var nombreAtributo = atributo.name;
            if (self.esDirectiva(nombreAtributo)) {
                var expresion = atributo.value;
                var directiva = nombreAtributo.substring(2);
                if (self.esDirectivaEvento(directiva)) {  
                    self.compilarEvento(nodo, self.vm, expresion, directiva);
                } else {  
                    self.compilarModelo(nodo, self.vm, expresion, directiva);
                }
                nodo.removeAttribute(nombreAtributo);
            }
        });
    },
    compilarTexto: function(nodo, expresion) {
        var self = this;
        var textoInicial = this.vm[expresion];
        this.actualizarTexto(nodo, textoInicial);
        new ObservadorVue(this.vm, expresion, function(valor) {
            self.actualizarTexto(nodo, valor);
        });
    },
    compilarEvento: function(nodo, vm, expresion, directiva) {
        var tipoEvento = directiva.split(':')[1];
        var callback = vm.metodos && vm.metodos[expresion];

        if (tipoEvento && callback) {
            nodo.addEventListener(tipoEvento, callback.bind(vm), false);
        }
    },
    compilarModelo: function(nodo, vm, expresion, directiva) {
        var self = this;
        var valor = this.vm[expresion];
        this.actualizarModelo(nodo, valor);
        new ObservadorVue(this.vm, expresion, function(valor) {
            self.actualizarModelo(nodo, valor);
        });

        nodo.addEventListener('input', function(e) {
            var nuevoValor = e.target.value;
            if (valor === nuevoValor) {
                return;
            }
            self.vm[expresion] = nuevoValor;
            valor = nuevoValor;
        });
    },
    actualizarTexto: function(nodo, valor) {
        node.textContent = typeof valor == 'undefined' ? '' : valor;
    },
    actualizarModelo: function(nodo, valor) {
        node.value = typeof valor == 'undefined' ? '' : valor;
    },
    esDirectiva: function(atributo) {
        return atributo.indexOf('v-') === 0;
    },
    esDirectivaEvento: function(directiva) {
        return directiva.indexOf('on:') === 0;
    },
    esNodoElemento: function(nodo) {
        return nodo.nodeType === 1;
    },
    esNodoTexto: function(nodo) {
        return nodo.nodeType === 3;
    }
}

Etiquetas: vue.js enlace-datos Object.defineProperty Observer Watcher

Publicado el 7-20 12:13