Escribir un marco de JavaScript – Introducción al enlace de datos, más allá de la comprobación sucia

En este capítulo, te explicaré las técnicas de comprobación sucia y de enlace de datos de accesores y señalaré sus fortalezas y debilidades.

Una introducción al enlace de datos

El enlace de datos es una técnica general que une los orígenes de datos del proveedor y el consumidor y los sincroniza.

Esta es una definición general, que describe los bloques de construcción comunes de las técnicas de enlace de datos.

  • Una sintaxis para definir el proveedor y el consumidor.
  • Una sintaxis para definir qué cambios deberían desencadenar la sincronización.
  • Una forma de escuchar estos cambios en el proveedor.
  • Una función de sincronización que se ejecuta cuando ocurren estos cambios. De ahora en adelante llamaré a esta función hhandler()andler().

Los pasos anteriores se implementan de diferentes maneras mediante las diferentes técnicas de enlace de datos. Las próximas secciones tratarán sobre dos técnicas de este tipo, a saber, la verificación de errores y el método de acceso. Ambos tienen sus fortalezas y debilidades, las cuales trataré brevemente después de presentarlas.

Control sucio

La comprobación sucia es probablemente el método de enlace de datos más conocido. Es de concepto simple, y no requiere características de lenguaje complejas, lo que lo convierte en un buen candidato para el uso heredado.

La sintaxis

Definir el proveedor y el consumidor no requiere ninguna sintaxis especial, solo objetos simples de Javascript.


const provider = {
message: 'Hola Mundo'
}
const consumer = document.createElement('p')

La sincronización generalmente se desencadena por mutaciones de propiedades en el proveedor. Las propiedades que deben observarse para los cambios deben ser mapeadas explícitamente con sus handler().


observe(provider, 'message', message => {
consumer.innerHTML = message
})

La función observe() simplemente guarda la asignación (provider, property) -> handler para su uso posterior.


function observe (provider, prop, handler) {
provider._handlers[prop] = handler
}

Con esto, tenemos una sintaxis para definir el proveedor y el consumidor, y una forma de registrar funciones handler() para los cambios de propiedad. La API pública de nuestra biblioteca está lista, ahora viene la implementación interna.

Escuchando sobre los cambios

La comprobación sucia se llama sucia por una razón. Ejecuta controles periódicos en lugar de escuchar directamente los cambios de propiedad. Llamemos a este cheque un ciclo de digestión de ahora en adelante. Un ciclo de resumen se itera a través de cada entrada (provider, property) -> handler agregada a observe() y verifica si el valor de la propiedad cambió desde la última iteración. Si cambió, ejecuta la función handler(). Una implementación simple se vería a continuación.


function digest () {
providers.forEach(digestProvider)
}

function digestProvider (provider) {
for (let prop in provider._handlers) {
if (provider._prevValues[prop] !== provider[prop]) {
provider._prevValues[prop] = provider[prop]
handler(provider[prop])
}
}
}

La función digest() debe ejecutarse de vez en cuando para garantizar un estado sincronizado.

La técnica accesora.

La técnica de acceso es la tendencia actual. Es un poco menos ampliamente compatible ya que requiere la funcionalidad de obtención / configuración de ES5, pero esto lo compensa con elegancia.

La sintaxis

La definición del proveedor requiere una sintaxis especial. El objeto de proveedor plano debe pasarse a la función observer(), que lo transforma en un objeto observable.


const provider = observer({
greeting: 'Hola',
subject: 'Mundo'
})
const consumer = document.createElement('p')

Este pequeño inconveniente está más que compensado por la sintaxis de mapeo simple handler(). Con la verificación sucia, tendríamos que definir cada propiedad observada explícitamente como se muestra a continuación.


observer(provider, 'greeting', greeting => {
consumer.innerHTML = greeting + ' ' + provider.subject
})

observe(provider, 'subject', subject => {
consumer.innerHTML = provider.greeting + ' ' + subject
})

Esto es detallado y torpe. La técnica de acceso puede detectar automáticamente las propiedades del proveedor utilizado dentro de la función handler(), lo que nos permite simplificar el código anterior.


observer(() => {
consumer.innerHTML = provider.greeting + ' ' + provider.subject
})

La implementación de observe() es diferente de la comprobación sucia. Simplemente ejecuta la función handler() pasada y la marca como activa actualmente mientras se está ejecutando.


let activeHandler

function observe(handler) {
activeHandler = handler
handler()
activeHandler = undefined
}

Ten en cuenta que explotamos la naturaleza de un solo hilo de JavaScript aquí mediante el uso de la única variable activeHandler para realizar un seguimiento de la función handler() actualmente en ejecución.

Escuchando sobre los cambios

Aquí es de donde viene el nombre de ‘técnica accesoria’. El proveedor se complementa con getters / setters, que hacen el trabajo pesado en el fondo. La idea es interceptar las operaciones de obtención / ajuste de las propiedades del proveedor de la siguiente manera.

  • get: si hay una ejecución activeHandler, guardar la asignación (provider, property) -> activeHandler para su uso posterior.
  • set: Ejecutar todas las funciones handler(), que se asignan con el par (provide, property).

La técnica de enlace de datos de acceso.

El siguiente código muestra una implementación simple de esto para una propiedad de un solo proveedor.


function observableProp (provider, prop) {
const value = provider[prop]
Object.defineProperty(provider, prop, {
get () {
if (activeHandler) {
provider._handlers[prop] = activeHandler
}
return value
},
set (newValue) {
value = newValue
const handler = obj._handlers[prop]
if (handler) {
activeHandler = handler
handler()
activeHandler = undefined
}
}
})
}

La función observable() mencionada en la sección anterior recorre las propiedades del proveedor y las convierte en observables con la función anterior observableProp().

[/php]

function observable (provider) {
for (let prop in provider) {
observableProp(provider, prop)
if (typeof provider[prop] === ‘object’) {
observable(provider[prop])
}
}
}

[/php]

Esta es una implementación muy simple, pero es suficiente para una comparación entre las dos técnicas.

Comparación de las técnicas.

En esta sección, describiré brevemente las fortalezas y debilidades de la verificación sucia y la técnica de acceso.

Sintaxis

La comprobación sucia no requiere una sintaxis para definir el proveedor y el consumidor, pero asignar el par (provider,property) handler() es torpe y no flexible.

La técnica de acceso requiere que el proveedor se ajuste a la función observable(), pero la asignación automática handler() lo compensa. Para grandes proyectos con enlace de datos, es una característica imprescindible.

Actuación

La comprobación sucia es notoria por su mal desempeño. Tienes que verificar cada entrada (provider, property) -> handler posiblemente varias veces durante cada ciclo de resumen. Además, tienes que moler incluso cuando la aplicación está inactiva, ya que no puede saber cuándo suceden los cambios de propiedad.

El método de acceso es más rápido, pero el rendimiento podría degradarse innecesariamente en el caso de objetos observables grandes. Reemplazar cada propiedad del proveedor por accesores es generalmente una exageración. Una solución sería construir el árbol getter / setter dinámicamente cuando sea necesario, en lugar de hacerlo por adelantado en un lote. Alternativamente, una solución más simple es envolver las propiedades innecesarias con una función noObserve(), que le indica a observable() que deje esa parte sin tocar. Esto lamentablemente introduce alguna sintaxis extra.

Flexibilidad

La verificación sucia, naturalmente, funciona tanto con las propiedades expando (agregadas dinámicamente) como con las propiedades de acceso.

La técnica de acceso tiene un punto débil aquí. Las propiedades de Expando no son compatibles porque se quedan fuera del árbol de obtención / establecimiento inicial. Esto causa problemas con las matrices, por ejemplo, pero se puede solucionar ejecutando manualmente observableProp() después de agregar una nueva propiedad. Las propiedades de funciónno son compatibles ya que los accessors no pueden ser envueltos por los accessors nuevamente. Una solución común para esto es usar una función computed() en lugar de un getter. Esto introduce aún más sintaxis personalizada.

Alternativas de tiempo

La verificación sucia no nos da mucha libertad aquí, ya que no tenemos forma de saber cuándo suceden los cambios reales en la propiedad. Las funciones handler() solo se pueden ejecutar de forma asíncrona, ejecutando el ciclo digest() de vez en cuando.

Los activadores / definidores agregados mediante la técnica de accesores se activan de forma síncrona, por lo que tenemos libertad de elección. Podemos decidir ejecutar el archivo handler() de inmediato o guardarlo en un lote que se ejecuta de forma asíncrona más tarde. El primer enfoque nos da la ventaja de la previsibilidad, mientras que el segundo permite mejoras de rendimiento mediante la eliminación de duplicados.