Muchos frameworks front-end de JavaScript (como Angular, React y Vue) tienen sus propios sistemas de reactividad. Comprender las características de estos sistemas será útil para cualquier desarrollador, lo ayudará a usar de manera más eficiente los marcos JS modernos.

El material, cuya traducción publicamos hoy, muestra un ejemplo paso a paso de desarrollar un sistema de reactividad en JavaScript puro. Este sistema implementa los mismos mecanismos que se usan en Vue.
Sistema de reactividad
Para alguien que se encuentra por primera vez con el sistema de reactividad Vue, puede parecer una misteriosa caja negra. Considere una aplicación Vue simple. Aquí está el marcado:
<div id="app"> <div>Price: ${{ price }}</div> <div>Total: ${{ price*quantity }}</div> <div>Taxes: ${{ totalPriceWithTax }}</div> </div>
Aquí está el comando de conexión de framework y el código de la aplicación.
<script src="https://cdn.jsdelivr.net/npm/vue"></script> <script> var vm = new Vue({ el: '#app', data: { price: 5.00, quantity: 2 }, computed: { totalPriceWithTax() { return this.price * this.quantity * 1.03 } } }) </script>
De alguna manera, Vue descubre que cuando el
price
cambia, el motor necesita hacer tres cosas:
- Actualizar el valor del
price
en la página web. - Vuelva a calcular la expresión en la que el
price
se multiplica por la quantity
y muestre el valor resultante en la página. - Llame a la función
totalPriceWithTax
y, nuevamente, coloque lo que devuelve en la página.
Lo que sucede aquí se muestra en la siguiente ilustración.
¿Cómo sabe Vue qué hacer cuando cambia la propiedad del precio?Ahora tenemos preguntas sobre cómo Vue sabe qué necesita actualizarse exactamente cuando cambia el
price
, y cómo el motor rastrea lo que está sucediendo en la página. Lo que puede observar aquí no parece una aplicación JS normal.
Quizás esto aún no sea obvio, pero el problema principal que debemos resolver aquí es que los programas JS generalmente no funcionan así. Por ejemplo, ejecutemos el siguiente código:
let price = 5 let quantity = 2 let total = price * quantity
¿Qué crees que se mostrará en la consola? Como aquí no se usa nada excepto JS normal,
10
llegará a la consola.
El resultado del programa.Y cuando utilizamos las capacidades de Vue, en una situación similar, podemos implementar un escenario en el que se recuenta el valor
total
cuando cambian las variables de
price
o
quantity
. Es decir, si el sistema de reactividad se usara en la ejecución del código anterior, entonces no 10, sino 40 se mostrarían en la consola:
Salida de consola generada por código hipotético utilizando un sistema de reactividadJavaScript es un lenguaje que puede funcionar tanto de forma procesal como orientado a objetos, pero no tiene un sistema de reactividad incorporado, por lo que el código que consideramos al cambiar el
price
no enviará el número 40 a la consola. Para que el indicador
total
sea recalculado cuando el
price
o la
quantity
cambien, necesitaremos crear un sistema de reactividad por nuestra cuenta y así lograr el comportamiento que necesitamos. Vamos a dividir el camino hacia este objetivo en varios pasos pequeños.
Tarea: almacenamiento de reglas para calcular indicadores
Necesitamos un lugar para guardar información sobre cómo se calcula el indicador
total
, lo que nos permitirá volver a calcularlo al cambiar los valores de las variables de
price
o
quantity
.
▍Solución
Primero, debemos decirle a la aplicación lo siguiente: "Aquí está el código que voy a ejecutar, guárdelo, es posible que necesite ejecutarlo en otro momento". Entonces tendremos que ejecutar el código. Más tarde, si los indicadores de
price
o
quantity
han cambiado, deberá llamar al código guardado para volver a calcular el
total
. Se ve así:
El código de cálculo total debe guardarse en algún lugar para poder acceder a él más tarde.El código al que puede llamar en JavaScript para realizar alguna acción está formateado como funciones. Por lo tanto, escribiremos una función que se ocupe del cálculo del
total
, y también crearemos un mecanismo para almacenar funciones que podamos necesitar más adelante.
let price = 5 let quantity = 2 let total = 0 let target = null target = function () { total = price * quantity } record()
Tenga en cuenta que almacenamos la función anónima en la variable de
target
y luego llamamos a la función de
record
. Hablaremos de eso a continuación. También me gustaría señalar que la función de
target
, utilizando la sintaxis de las funciones de flecha ES6, se puede reescribir de la siguiente manera:
target = () => { total = price * quantity }
Aquí está la declaración de la función de
record
y la estructura de datos utilizada para almacenar las funciones:
let storage = [] // target function record () { // target = () => { total = price * quantity } storage.push(target) }
Usando la función de
record
, guardamos la función de
target
(en nuestro caso
{ total = price * quantity }
) en la matriz de
storage
, lo que nos permite llamar a esta función más tarde, posiblemente usando la función de
replay
, cuyo código se muestra a continuación. Esto nos permitirá llamar a todas las funciones almacenadas en el
storage
.
function replay () { storage.forEach(run => run()) }
Aquí revisamos todas las funciones anónimas almacenadas en la matriz de
storage
y ejecutamos cada una de ellas.
Luego, en nuestro código podemos hacer lo siguiente:
price = 20 console.log(total) // 10 replay() console.log(total) // 40
No todo parece tan difícil, ¿verdad? Aquí está todo el código, cuyos fragmentos discutimos anteriormente, en caso de que sea más conveniente para usted tratarlo finalmente. Por cierto, este código no se escribe accidentalmente de esa manera.
let price = 5 let quantity = 2 let total = 0 let target = null let storage = [] function record () { storage.push(target) } function replay () { storage.forEach(run => run()) } target = () => { total = price * quantity } record() target() price = 20 console.log(total)
Esto es lo que se mostrará en la consola del navegador después de que comience.
Resultado del códigoReto: una solución confiable para almacenar funciones
Podemos continuar escribiendo las funciones que necesitamos cuando sea necesario, pero sería bueno si tuviéramos una solución más confiable que se pueda escalar con la aplicación. Quizás sea una clase que mantenga una lista de funciones escritas originalmente en la variable de
target
y que reciba notificaciones si necesitamos volver a ejecutar estas funciones.
▍Solución: clase de dependencia
Un enfoque para resolver el problema anterior es encapsular el comportamiento que necesitamos en una clase, que se puede llamar dependencia. Esta clase implementará el patrón de programación estándar del observador.
Como resultado, si creamos una clase JS utilizada para administrar nuestras dependencias (que estará cerca de cómo se implementan mecanismos similares en Vue), podría verse así:
class Dep { // Dep - Dependency constructor () { this.subscribers = [] // , // notify() } depend () { // record if (target && !this.subscribers.includes(target)){ // target // this.subscribers.push(target) } } notify () { // replay this.subscribers.forEach(sub => sub()) // - } }
Tenga en cuenta que en lugar de la matriz de
storage
, ahora almacenamos nuestras funciones anónimas en la matriz de
subscribers
. En lugar de la función de
record
, ahora se llama al método
depend
. También aquí, en lugar de la función de
replay
, se
notify
función de
notify
. Aquí se explica cómo ejecutar nuestro código con la clase
Dep
:
const dep = new Dep() let price = 5 let quantity = 2 let total = 0 let target = () => { total = price * quantity } dep.depend()
Nuestro nuevo código funciona igual que antes, pero ahora está mejor diseñado y parece que es mejor reutilizarlo.
Lo único que parece extraño hasta ahora es trabajar con una función almacenada en la variable de
target
.
Tarea: mecanismo para crear funciones anónimas
En el futuro, necesitaremos crear un objeto de clase
Dep
para cada variable. Además, sería bueno encapsular el comportamiento de crear funciones anónimas en algún lugar, al que se debe llamar al actualizar los datos relevantes. Quizás esto nos ayude con una función adicional, que llamaremos
watcher
. Esto llevará al hecho de que podemos reemplazar esta construcción del ejemplo anterior con una nueva función:
let target = () => { total = price * quantity } dep.depend() target()
De hecho, una llamada a la función de
watcher
que reemplaza este código se verá así:
watcher(() => { total = price * quantity })
▍ Solución: función de observador
Dentro de la función de
watcher
, cuyo código se presenta a continuación, podemos realizar varias acciones simples:
function watcher(myFunc) { target = myFunc // target myFunc dep.depend() // target target() // target = null // target }
Como puede ver, la función de
watcher
toma, como argumento, la función
myFunc
, la escribe en la variable
target
global, llama a
dep.depend()
para agregar esta función a la lista de suscriptores, llama a esta función y restablece la variable
target
.
Ahora obtenemos los mismos valores 10 y 40 si ejecutamos el siguiente código:
price = 20 console.log(total) dep.notify() console.log(total)
Quizás se pregunte por qué implementamos
target
como una variable global, en lugar de pasar esta variable a nuestras funciones, si es necesario. Tenemos buenas razones para hacer eso, luego lo comprenderá.
Tarea: propio objeto Dep para cada variable
Tenemos un solo objeto de clase
Dep
. ¿Qué sucede si necesitamos que cada una de nuestras variables tenga su propio objeto de clase
Dep
? Antes de continuar, muevamos los datos con los que trabajamos a las propiedades del objeto:
let data = { price: 5, quantity: 2 }
Imagine por un momento que cada una de nuestras propiedades (
price
y
quantity
) tiene su propio objeto interno de clase
Dep
.
Propiedades de precio y cantidadAhora podemos llamar a la función de
watcher
esta manera:
watcher(() => { total = data.price * data.quantity })
Como estamos trabajando aquí con el valor de la propiedad
data.price
, necesitamos el objeto de clase
Dep
de la propiedad
price
para colocar una función anónima (almacenada en
target
) en su matriz de suscriptores (llamando a
dep.depend()
). Además, dado que estamos trabajando con
data.quantity
, necesitamos el objeto
Dep
de la propiedad de
quantity
para colocar una función anónima (nuevamente, almacenada en el
target
) en su matriz de suscriptores.
Si representa esto en forma de diagrama, obtiene lo siguiente.
Las funciones se dividen en matrices de suscriptores de objetos de clase Dep correspondientes a diferentes propiedadesSi tenemos una función anónima más en la que trabajamos solo con la propiedad
data.price
, entonces la función anónima correspondiente solo debe ir al Depósito del objeto de esta propiedad.
Se pueden agregar observadores adicionales a solo una de las propiedades disponibles.¿Cuándo podría necesitar llamar a
dep.notify()
para funciones suscritas a cambios en la propiedad de
price
? Esto será necesario al cambiar el
price
. Esto significa que cuando nuestro ejemplo esté completamente listo, el siguiente código debería funcionar para nosotros.
Aquí, al cambiar el precio, debe llamar a dep.notify () para todas las funciones suscritas al cambio de precioPara que todo funcione de esta manera, necesitamos alguna forma de interceptar los eventos de acceso a la propiedad (en nuestro caso, es el
price
o la
quantity
). Esto permitirá, cuando esto ocurra, guardar la función de
target
en una matriz de suscriptores, y cuando cambie la variable correspondiente, ejecutar la función almacenada en esta matriz.
▍Solución: Object.defineProperty ()
Ahora debemos familiarizarnos con el método estándar ES5
Object.defineProperty (). Le permite asignar captadores y definidores a las propiedades de los objetos. Permítanme, antes de pasar a su uso práctico, demostrar el funcionamiento de estos mecanismos con un simple ejemplo.
let data = { price: 5, quantity: 2 } Object.defineProperty(data, 'price', { // price get() { // console.log(`I was accessed`) }, set(newVal) { // console.log(`I was changed`) } }) data.price // data.price = 20 //
Si ejecuta este código en la consola del navegador, mostrará el siguiente texto.
Resultados de obtención y fijaciónComo puede ver, nuestro ejemplo simplemente imprime un par de líneas de texto en la consola. Sin embargo, no lee ni establece valores, ya que redefinimos la funcionalidad estándar de getters y setters. Restauraremos la funcionalidad de estos métodos. Es decir, se espera que los captadores devuelvan los valores de los métodos correspondientes y los establecedores los establezcan. Por lo tanto, agregaremos una nueva variable,
internalValue
, al código, que usaremos para almacenar el valor del
price
actual.
let data = { price: 5, quantity: 2 } let internalValue = data.price // Object.defineProperty(data, 'price', { // price get() { // console.log(`Getting price: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting price to: ${newVal}`) internalValue = newVal } }) total = data.price * data.quantity // data.price = 20 //
Ahora que getter y setter funcionan de la forma en que deberían funcionar, ¿qué crees que entrará en la consola cuando se ejecute este código? Echa un vistazo a la siguiente figura.
Salida de datos a la consolaEntonces, ahora tenemos un mecanismo que le permite recibir notificaciones al leer los valores de las propiedades y cuando se escriben nuevos valores en ellos. Ahora, habiendo reelaborado un poco el código, podemos equipar a los captadores y definidores con todas las propiedades del objeto de
data
. Aquí usaremos el método
Object.keys()
, que devuelve una matriz de claves del objeto que se le pasó.
let data = { price: 5, quantity: 2 } Object.keys(data).forEach(key => { // data let internalValue = data[key] Object.defineProperty(data, key, { get() { console.log(`Getting ${key}: ${internalValue}`) return internalValue }, set(newVal) { console.log(`Setting ${key} to: ${newVal}`) internalValue = newVal } }) }) let total = data.price * data.quantity data.price = 20
Ahora todas las propiedades del objeto de
data
tienen captadores y definidores. Esto es lo que aparece en la consola después de ejecutar este código.
Salida de datos a la consola por getters y settersAsamblea del sistema de reactividad
Cuando
total = data.price * data.quantity
un fragmento de código como
total = data.price * data.quantity
y se obtiene el valor de la propiedad de
price
, necesitamos que la propiedad de
price
"recuerde" la función anónima correspondiente (
target
en nuestro caso). Como resultado, si se cambia la propiedad del
price
, es decir, se establece en un nuevo valor, esto llevará a una llamada a esta función para repetir las operaciones que realiza, ya que sabe que una determinada línea de código depende de ello. Como resultado, las operaciones realizadas en getters y setters se pueden imaginar de la siguiente manera:
- Getter: debe recordar la función anónima, a la que llamaremos nuevamente cuando cambie el valor.
- Setter: es necesario ejecutar la función anónima almacenada, lo que conducirá a un cambio en el valor resultante correspondiente.
Si usa la clase
Dep
ya conocida en esta descripción, obtendrá lo siguiente:
- Al leer un valor de propiedad, se llama a
dep.depend()
para guardar la función de target
actual. - Cuando se escribe un valor en una propiedad, se llama a
dep.notify()
para reiniciar todas las funciones almacenadas.
Ahora combinaremos estas dos ideas y, finalmente, llegaremos al código que nos permite lograr nuestro objetivo.
let data = { price: 5, quantity: 2 } let target = null // - , class Dep { constructor () { this.subscribers = [] } depend () { if (target && !this.subscribers.includes(target)){ this.subscribers.push(target) } } notify () { this.subscribers.forEach(sub => sub()) } } // , // Object.keys(data).forEach(key => { let internalValue = data[key] // // Dep const dep = new Dep() Object.defineProperty(data, key, { get() { dep.depend() // target return internalValue }, set(newVal) { internalValue = newVal dep.notify() // } }) }) // watcher dep.depend(), // function watcher(myFunc){ target = myFunc target() target = null } watcher(() => { data.total = data.price * data.quantity })
Experimentemos con este código en la consola del navegador.
Listo experimentos de códigoComo puede ver, ¡funciona exactamente como lo necesitamos! ¡Las propiedades de
price
y
quantity
han vuelto reactivas! Todo el código que es responsable de generar el
total
cuando el
price
o la
quantity
cambia se ejecuta repetidamente.
Ahora, después de haber escrito nuestro propio sistema de reactividad, esta ilustración de la documentación de Vue le resultará familiar y comprensible.
Sistema de reactividad vue¿Ves este hermoso círculo púrpura que dice
contienen captadores y establecedores? Ahora debería estar familiarizado con usted. Cada instancia del componente tiene una instancia del método del observador (círculo azul), que recopila dependencias de los captadores (línea roja). Cuando, más tarde, se llama al setter, se le notifica al método del observador, lo que conduce a la representación del componente. Aquí está el mismo esquema, provisto de explicaciones que lo conectan con nuestro desarrollo.
Vue diagrama de reactividad con explicacionesCreemos que ahora, después de haber escrito nuestro propio sistema de reactividad, este esquema no necesita explicaciones adicionales.
Por supuesto, en Vue, todo esto es más complicado, pero ahora debe comprender el mecanismo subyacente en los sistemas de reactividad.
Resumen
Después de leer este material, aprendiste lo siguiente:
- Cómo crear una clase
Dep
que recopile funciones utilizando el método depend
y, si es necesario, las llame nuevamente utilizando el método de notify
. - Cómo crear una función de
watcher
que le permita controlar el código que ejecutamos (esta es la función de target
), que puede necesitar guardar en el objeto de la clase Dep
. - Cómo usar el método
Object.defineProperty()
para crear captadores y establecedores.
Todo esto, compilado en un solo ejemplo, condujo a la creación de un sistema de respuesta en JavaScript puro, al comprender que puede comprender las características del funcionamiento de dichos sistemas utilizados en los marcos web modernos.
Estimados lectores! Si, antes de leer este material, imaginaba mal las características de los mecanismos de los sistemas de reactividad, dígame, ¿ha logrado lidiar con ellos?