wandres.dev
TRACKING AUTOMÁTICO · Cómo sabe de qué depende

Recolectar dependencias durante la ejecución

El descubrimiento de aristas ocurre como efecto secundario de leer, no como un análisis previo. Qué implica que el grafo se construya ejecutando, y por qué eso hace el sistema exacto y a la vez impredecible.

⏱ 17 min

Un motor con tracking automático no analiza tu código: lo ejecuta y anota lo que pasó. Las aristas del grafo no se deducen del texto del programa sino que se observan durante una ejecución concreta con unos valores concretos. Esa diferencia entre analizar y observar tiene consecuencias enormes, y explica a la vez la precisión asombrosa del modelo y su resistencia a cualquier análisis estático.

🎯 Al terminar esta lección sabrás
  • Trazar el orden exacto de operaciones durante una recolección.
  • Ver que las aristas reflejan la ejecución real y no el texto del programa.
  • Entender por qué la recolección atraviesa fronteras de función sin coste.
  • Reconocer las duplicaciones y por qué el motor tiene que deduplicar.

La secuencia exacta

Cuando una computación se reejecuta, el orden es rígido y cada paso tiene su razón.

function ejecutar(nodo) {
  desatar(nodo);                     // 1. borrar todas las aristas de la pasada anterior
  const anterior = Observador;
  Observador = nodo;                 // 2. abrir el ambito de recoleccion
  try {
    nodo.valor = nodo.fn();          // 3. ejecutar: cada lectura teje una arista
  } finally {
    Observador = anterior;           // 4. cerrar el ambito
    nodo.estado = LIMPIO;
  }
}

El paso 1 antes que el 3 es obligatorio y no negociable: si no se borran las aristas viejas, las de la pasada anterior conviven con las nuevas y el nodo acaba suscrito a fuentes que ya no lee. La lección siguiente va entera sobre ese paso.

El paso 3 es donde ocurre todo. Cada llamada a un getter reactivo dentro de nodo.fn() ejecuta el tejido. El motor no sabe cuántas habrá ni cuáles serán: se entera al pasar por ellas.

Las aristas son de la ejecución, no del código

Esta es la propiedad central y conviene verla con un ejemplo donde el texto y la ejecución divergen.

const modo = senal('resumen');
const a = senal(1), b = senal(2), c = senal(3);

const vista = memo(() => {
  if (modo() === 'resumen') return a();
  if (modo() === 'detalle') return a() + b();
  return a() + b() + c();
});

Textualmente, vista menciona cuatro fuentes. En la ejecución con modo igual a 'resumen', teje exactamente dos aristas: modo y a. Ni b ni c aparecen en el grafo. Escribir en b no reejecuta nada, y no porque el motor haya decidido ignorarlo, sino porque no existe la arista.

Un análisis estático del texto no puede llegar a esa conclusión: tendría que evaluar la condición, que depende de un valor de tiempo de ejecución. Un analizador conservador tendría que suponer que las cuatro fuentes son dependencias, y el nodo se reejecutaría ante cambios en b y c sin necesidad. La recolección durante la ejecución es más precisa que cualquier análisis estático posible, y esa precisión es gratuita.

flowchart LR
M[modo] --> V[memo vista]
A[a] --> V
B[b] -.no hay arista.-> V
C[c] -.no hay arista.-> V
style M fill:#89b4fa,color:#11111b
style A fill:#89b4fa,color:#11111b
style B fill:#f9e2af,color:#11111b
style C fill:#f9e2af,color:#11111b
style V fill:#cba6f7,color:#11111b

Atraviesa fronteras de función

La segunda propiedad, y la que hace que el modelo escale, es que la recolección no se detiene en los límites de una función. La variable de observador actual es de módulo, así que cualquier lectura ejecutada durante el ámbito —a la profundidad de llamadas que sea— se atribuye a la computación abierta.

function reglaDeDescuento() {
  if (esVip()) return 0.2;          // lee una senal, tres marcos de pila mas abajo
  if (unidades() > 10) return 0.1;
  return 0;
}

function precioFinal() {
  return base() * (1 - reglaDeDescuento());
}

const total = memo(() => precioFinal());   // teje aristas a base, esVip y quiza unidades

Ninguna de las dos funciones auxiliares sabe nada del sistema reactivo. No reciben el nodo, no declaran dependencias, no importan nada del motor. Y aun así sus lecturas quedan registradas correctamente en total.

Esto es lo que permite abstraer lógica de negocio en funciones normales y usarla desde cualquier contexto reactivo. Compáralo con la declaración manual del nivel 2, donde las dependencias de una función forman parte de su firma pública. Aquí no forman parte de nada: son un efecto secundario de llamarla.

💡
De aqui salen los composables

Toda la práctica de encapsular lógica reactiva en funciones reutilizables —los composables de Vue, los primitivos de Solid, las funciones que devuelven señales en Angular— se apoya en esta propiedad. Una función que lee y devuelve señales es reutilizable en cualquier contexto reactivo sin declarar nada, porque el ámbito lo pone quien llama y no quien está escrito. Es la razón por la que la composición en estos motores no necesita ninguna convención especial: son funciones normales.

Duplicados y el coste de la precisión

Duplicados y deduplicación

Un cuerpo puede leer la misma fuente varias veces.

const etiqueta = memo(() => {
  if (contador() === 0) return 'vacio';
  return `${contador()} elementos, el ultimo es el ${contador()}`;
});

Tres lecturas de contador en una ejecución. Si el motor teje tres aristas, la propagación notificará tres veces al mismo nodo y el trabajo se triplica en cada escritura.

Cada representación resuelve el duplicado a su manera. Con conjuntos hash, la deduplicación es automática porque add sobre un elemento existente no hace nada. Con arrays hay que comprobar explícitamente si la arista ya existe, y aquí aparece una optimización bonita: como es abrumadoramente probable que la lectura repetida sea la última tejida, basta con comparar contra el último elemento del array para atrapar el caso común en tiempo constante. Con listas enlazadas y reutilización, el propio mecanismo de recorrido detecta que la fuente ya está enlazada en esta pasada.

// Deduplicacion barata para la representacion de arrays
function seguir(fuente) {
  if (!Observador) return;
  const f = Observador.fuentes;
  if (f && f[f.length - 1] === fuente) return;   // atrapa el caso comun
  // ... tejer la arista completa
}
El grafo es una fotografia de una ejecucion, no un modelo del programa

La consecuencia que cuesta más asimilar es epistemológica: el grafo no describe tu programa, describe una ejecución concreta de tu programa con unos valores concretos. Cambia un valor y el grafo puede ser otro. Esto tiene tres implicaciones que conviene tener presentes siempre. Primera: no existe el grafo de una aplicación, existen tantos como combinaciones de estado alcanzables, y por eso una herramienta de depuración solo puede enseñarte el actual. Segunda: una rama de código que nunca se ha ejecutado no tiene aristas, así que un bug de reactividad puede estar latente durante meses y aparecer la primera vez que alguien entra por ese camino; los tests que solo recorren el camino feliz no lo detectan. Tercera, y es la más profunda: un compilador no puede conocer el grafo, porque conocerlo equivaldría a resolver el problema de la parada. Por eso, cuando en el nivel 11 hablemos de compilar la reactividad, la conclusión será inevitablemente que el compilador puede optimizar la maquinaria del tracking —eliminar llamadas, alinear getters, precalcular plantillas— pero no puede sustituir el descubrimiento de aristas por un análisis estático sin volverse conservador y perder justo la precisión que hacía valioso al modelo. La recolección durante la ejecución no es una implementación provisional a la espera de un compilador mejor: es la única forma de obtener el grafo exacto.

El coste de la precisión

Nada de esto es gratis. La recolección durante la ejecución significa que cada lectura de cada fuente en cada ejecución paga el tejido. Un cuerpo que lee veinte fuentes paga veinte comprobaciones y hasta cuarenta operaciones sobre estructuras de datos, además de su cómputo.

Por eso los motores optimizan tanto esta ruta: la comprobación de duplicado contra el último elemento, la reutilización de aristas de las listas enlazadas, la creación perezosa de las colecciones. Todo eso existe para abaratar el camino que se recorre millones de veces.

Y por eso, también, todos ofrecen una manera de leer sin rastrear. Cuando sabes que no quieres la dependencia, saltarse el tejido es una optimización legítima además de un cambio semántico.

⚔️ Demuestra que el grafo depende del estado
  1. Escribe un memo con tres ramas que lean conjuntos distintos de fuentes.
  2. Instrumenta el motor para volcar las aristas del memo después de cada ejecución.
  3. Recorre las tres ramas y comprueba que el conjunto de aristas es distinto en cada una.
  4. Escribe en una fuente que solo aparece en la rama no activa y verifica que no se reejecuta nada.