# Reactividad en profundidad
¡Ha llegado la hora de profundizar en el asunto! Una de las características más distintas de Vue es su discreto sistema de reactividad. Los modelos simplemente son objetos delegados (proxied) de JavaScript. Cuando los modifique, se actualizará la vista. Esto hace que el gestor de estados sea sencillo e intuitivo, pero también es importante entender como funciona para evitar algunos errores comunes. En esta sección, vamos a indagar en algunos detalles de bajo nivel del sistema de reactividad de Vue.
# ¿Que es Reactividad?
Este término surge en programación muy frecuente estos días, ¿pero que quiere decir cuando la gente lo menciona? Reactividad es una paradigma de programación que nos permite adaptar a cambios en una manera declarativa. El ejemplo canónico que la gente a menudo demuestra, porque es genial, es una hoja de cálculo de Excel.
Si pone el número 2 en la primera celda, y el número 3 en la segunda y pide la SUM, la hoja de cálculo le daría. No hay sorpresas allí. Pero si actualiza el primero número, la SUM se actualiza automáticamente, también.
JavaScript usualmente no funciona como esto. Si estuviéramos escribiendo algo similar en JavaScript:
let val1 = 2
let val2 = 3
let sum = val1 + val2
console.log(sum) // 5
val1 = 3
console.log(sum) // Still 5
2
3
4
5
6
7
8
9
Si actualizamos el primer valor, el sum no es adaptado.
Entonces, ¿cómo podríamos hacerlo en JavaScript?
Como una visión general de alto nivel, hay unas cuantas cosas que debemos ser capaz de hacer:
- Rastrear cuándo un valor esté leido. p. ej.
val1 + val2
lee tantoval1
comoval2
. - Detectar cuándo un valor se cambie. p. ej. cuando asignamos
val1 = 3
. - Reejecutar el código que lea el valor originalmente. p. ej. Ejecutar
sum = val1 + val2
de nuevo para actualizar el valor desum
.
No podemos hacerlo directamente utilizando el código desde el previo ejemplo, pero volverémos a este ejemplo más adelante para ver como adaptarlo para sea compatible con el sistema de reactividad de Vue.
Primero, vamos a profundizar un poco más en cómo Vue implementa los requisitos núcleos de reactividad descritos anteriormente.
# Cómo Vue sabe que código está ejecutando
Para ser capaz de obtener nuestra suma cada vez que los valores se cambien, la primera cosa que debemos hacer es envolverlo en una función:
const updateSum = () => {
sum = val1 + val2
}
2
3
Pero, ¿cómo informamos a Vue sobre esta función?
Vue realiza un seguimiento de cual función está ejecutando en la actualidad, mediante utilizar un effect. Un effect es una envoltura alrededor de la función que inicie el seguimiento justo antes de que se llame la función. Vue sabe cual effect está ejecutando en cualquier momento y puede ejecutarlo de nuevo cuando sea necesario.
Para comprenderlo mejor, tratemos de implementar algo similar nosotros, sin Vue, para ver cómo podría funcionar.
Lo que necesitamos es algo que pueda envolver nuestra suma, como esto:
createEffect(() => {
sum = val1 + val2
})
2
3
Necesitamos createEffect
para realizar un seguimiento de cuando la suma está ejecutando. Podríamos implementarlo de alguna manera como esto:
// Mantener una pila de _effects_ que están ejecutando
const runningEffects = []
const createEffect = fn => {
// Envolver la función pasada en una función _effect_
const effect = () => {
runningEffects.push(effect)
fn()
runningEffects.pop()
}
// Automáticamente ejecutar el _effect_ inmediatmente
effect()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Cuando nuestro effect está llamado, se empuja a sí mismo en la matriz runningEffects
, antes de llamar fn
. Cualquiera cosa que necesita saber cual effect está ejecutando en la actualidad puede revisar la matriz.
Effects actúan como el punto de partida para muchas características fundamentales. Por ejemplo, tanto la renderización de componentes como las propiedades computadas utilizan effects internalmente. Cualquier tiempo cuando algo responda a cambios de datos mágicamente, puede ser casi seguro que se haya envuelto en un effect.
Mientras la API pública de Vue no incluye nada de maneras para crear un effect directamente, en verdad expone una función llamada watchEffect
, lo que se porta muy similar como la función createEffect
de nuestro ejemplo. Discutirémoslo con más detalles más adelante en la guía.
Pero saber qué código está ejecutando es solo un parte del puzle. ¿Cómo sabe Vue qué valores utiliza el effect y cuando se cambian?
# Cómo Vue rastrear estos cambios
No podemos rastrear reasignación de variables locales como esos en nuestros ejemplos anteriores, simplemente no hay mecanismo para hacerlo en JavaScript. Los que podemos rastrear son los cambios de las propiedades de objetos.
Cuando retornamos un objeto JavaScript plano desde la función data
de un componente, Vue va a envolver ese objeto en un Proxy (opens new window) con manejadores para get
y set
. Los proxies estuvieron introducido en ES6 y le permite a Vue 3 a evitar unas de las advertencias de reactividad que existen en las versiones anteriores de Vue.
See the Pen Proxies y la Reactividad de Vue Explicados Visualmente by Vue (@Vue) on CodePen.
¡Eso fue muy rápido y requiere unos conocimientos de Proxies (opens new window) para comprender! Por eso dejamos profundizar un poco. Hay muchos para conocer sobre Proxies, pero lo que en realidad necesita saber es que un Proxy es un objeto que encierra otro objeto y le permite interceptar cualquier interacción con el objeto encerrado.**
Utilizámoslo como este: new Proxy(target, handler)
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log('intercepted!')
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// intercepted!
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Aquí hemos interceptado los intentos de leer propiedades del objeto objetivo. Una función de manejador como este es también conocido como una trampa. Hay muchos tipos diferentes de trampas disponibles, cada uno maneja un tipo diferente de interacción.
Más allá de un registro de la consola, podríamos hacer cualquier cosa que nos gustaríamos aquí. Podríamos incluso no retornar el valor verdadero si quisiéramos. Este hace que Proxies sea muy poderoso para crear API.
Un reto que se encuentra cuando se utiliza un Proxy es la vinculación de this
. Querríamos que cualquier método sea vinculado al Proxy en lugar del objeto objetivo, así que podemos interceptarlos también. Afortunadamente, ES6 introdujo otra nueva característica, llamada Reflect
, lo que nos permite eliminar este problema con mínimo esfuerzo:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
El primero paso hacia implementar reactividad con un Proxy es rastrear cuando una propiedad sea leido. Hacemoslo en el manejador, en una función llamada track
, a la que pasamos target
y property
:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
La implementación de track
no está mostrado aquí. Comprobará cual effect está ejecutando en la actualidad y lo grabará junto con target
y property
. Esto es cómo Vue sabe que la propiedad es una dependencia del effect.
Por fin, necesitamos reejecutar el effect cuando el valor de la propiedad haya cambiado. Para esto vamos a necesitar un manejador set
en nuestro Proxy:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
trigger(target, property)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
¿Recuerda esta lista de antes? Ahora tenemos algunas respuestas para cómo Vue implementa estos pasos fundamentales:
- Rastrear cuándo un valor esté leido: la función
track
en el manejadorget
del proxy graba la propiedad y el actual effect. - Detectar cuándo un valor se cambie: el manejador
set
está llamado en el proxy. - Reejecutar el código que lea el valor originalmente: la función
trigger
busca cuales effects dependen de la propiedad y los ejecute.
El objeto delegado (proxied) es invisible al usuario, pero fundamentalmente le permite a Vue a realizar el seguimiento de dependencias y notificación a cambios cuando las propiedades son accesado o modificado. Una advertencia es que el registro de la consola va a formatear los objetos delegados (proxied) de maneras diferentes, así que querría instalar vue-devtools (opens new window) para una interfaz más faborable a la inspección.
Si queremos reescribir nuestro ejemplo original utilizando un componente, podríamos hacerlo de una manera como esto:
const vm = createApp({
data() {
return {
val1: 2,
val2: 3
}
},
computed: {
sum() {
return this.val1 + this.val2
}
}
}).mount('#app')
console.log(vm.sum) // 5
vm.val1 = 3
console.log(vm.sum) // 6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
El objeto retornado por data
será envuelto en un proxy reactivo y almacenado como this.$data
. Las propiedades this.val1
y this.val2
son alias para this.$data.val1
y this.$data.val2
, respectivamente, po eso vienen del mismo proxy.
Vue envolverá la función para sum
en un effect. Cuando tratamos de acceder this.sum
, ejecutará ese effect para calcular el valor. El proxy reactivo alrededor $data
rastreará que las propiedades val1
y val2
sean leido cuando ese effect sea ejecutando.
A partir de Vue 3, nuestra reactividad es ahora disponible en un paquete separado (opens new window). La función que envolve $data
en un proxy es llamado reactive
. Podemos llamar este directamente por nosotros mismos, permitíendonos envolver un objeto en un proxy reactivo sin necesidad de utilizar un componente:
const proxy = reactive({
val1: 2,
val2: 3
})
2
3
4
Explorarémos la funcionalidad expuesta por el paquete de reactividad a lo largo del curso de las siguientes páginas de esta guía. Eso incluye funciones como reactive
y watchEffect
que ya hemos encontrado, así como maneras para utilizar otras características de reactividad, como computed
y watch
, sin necesidad de crear un componente.
# Objetos Delegados (Proxied)
Vue rastrea todos objetos que se han hecho reactivo internalmente, así que siempre retorna el mismo proxy para el mismo objeto.
Cuando un objeto anidado está accesado desde un proxy reactivo, ese objeto es también convertido en un proxy antes de ser retornado:
const handler = {
get(target, property, receiver) {
track(target, property)
const value = Reflect.get(...arguments)
if (isObject(value)) {
// Wrap the nested object in its own reactive proxy
return reactive(value)
} else {
return value
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
# Proxy versus la identidad original
El uso de Proxy de hecho introduce una nueva advertencia que deba tener en cuenta: el objeto delegado no es igual al objeto original en términos de la comparación de identidad (===
). Por ejemplo:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
2
3
4
Otras operacions que dependen de las comparaciones estrictas de igualdad pueden también ser afectado, como .includes()
o .indexOf()
.
La mejor práctica aquí es nunca poseer una referencia al crudo objeto original y solo trabajar con la versión reactiva:
const obj = reactive({
count: 0
}) // no referencia al original
2
3
Este asegura que tanto las comparaciones de igualdad como la reactividad se comportan como se espera.
Note que Vue no envolver los valores primitivos como los números o cadenas de caracteres en un proxy, por eso puede todavía utilizar ===
directamente con esos valores:
const obj = reactive({
count: 0
})
console.log(obj.count === 0) // true
2
3
4
5
# Cómo la renderización reacciona a cambios
La plantilla para un componente es compilado en una función render
. La función render
crea los VNodes (Nodos Virtuales) que describen como el componente se debe renderizar. Es envuelto en un effect, permitiendo que Vue rastree las propiedades que son 'tocado' mientras está ejecutando.
Una función render
es conceptualmente muy similar a la propiedad computed
. Vue no rastrea exactamente cómo las dependencias son utilizado, solo sabe que ellas hayan sido utilizado en algún momento mientras la función está ejecutando. Si cualquiera de estas propiedades cambia posteriormente, disparará el effect para ejecutar de nuevo, reejecutando la función render
para generar nuevos VNodes. Estos son luego utilizado para hacer los cambios necesarios al DOM.
See the Pen La Segunda Reactividad con Proxies en El Explicador de Vue 3 by Vue (@Vue) on CodePen.
Si está utilizando Vue 2.x y bajo, le podría interesar en algunas de las advertencias de detección de cambios que existen en esos versiones, explicados con más detalles aquí.