wandres.dev
ONTOLOGÍA · El mapa de los motores de reactividad

Qué hace exactamente un motor de reactividad

La definición mecánica: un motor de reactividad es una máquina que mantiene un conjunto de valores derivados consistente con sus entradas, recalculando lo mínimo. Ni magia ni azúcar sintáctico: una estructura de datos y un algoritmo de recorrido.

⏱ 16 min

La palabra reactividad se usa para tantas cosas que ha dejado de significar ninguna. Aquí no vamos a hablar de estilos de programación ni de paradigmas: vamos a abrir la caja. Un motor de reactividad es una máquina concreta, con una estructura de datos concreta y un algoritmo de recorrido concreto, cuyo trabajo es mantener un conjunto de valores derivados consistente con sus entradas gastando el mínimo cómputo posible. Todo lo demás —las señales, los efectos, los memos— son la interfaz que esa máquina expone.

🎯 Al terminar esta lección sabrás
  • Definir un motor de reactividad en términos de la invariante que garantiza.
  • Separar el contrato del motor de la sintaxis con la que se usa.
  • Identificar las tres operaciones primitivas: leer, escribir y propagar.
  • Reconocer por qué el problema es de grafos y no de eventos.

La invariante

Empieza por el contrato, no por la API. Un motor de reactividad gestiona dos clases de valor. Los valores de entrada, que alguien externo escribe: la posición del ratón, la respuesta de una petición, lo que el usuario teclea. Y los valores derivados, definidos por una función pura de otros valores del sistema: el total de un carrito, si un formulario es válido, el texto que debe aparecer en un nodo del DOM.

La invariante que promete el motor es esta: en todo momento observable, cada valor derivado es igual al resultado de aplicar su función a los valores actuales de sus entradas. Dicho de otro modo, el sistema nunca te deja ver una derivación desactualizada.

Cumplir esa invariante es trivial si te da igual el coste: recalcula todo tras cada escritura. Un bucle que reevalúa las mil derivaciones del programa cada vez que cambia un byte satisface la invariante perfectamente. El motor existe porque ese coste es inaceptable, y por tanto el problema real no es garantizar la consistencia, sino garantizarla recalculando lo mínimo. Esa segunda mitad de la frase es donde vive toda la ingeniería de este track.

ℹ️
Reactividad no es el patrón observador

El patrón observador resuelve la notificación: A avisa a B. Un motor de reactividad resuelve la consistencia: A cambia y el sistema entero vuelve a un estado correcto, en el orden correcto, sin estados intermedios visibles y sin recalcular lo que no hace falta. El observador es una arista; el motor es el grafo completo más el algoritmo que lo recorre.

Tres operaciones y una estructura

Reducido a lo esencial, cualquier motor de esta familia expone tres operaciones sobre una estructura compartida.

Leer un valor. Devuelve el valor actual y, si la lectura ocurre dentro de una derivación, registra una dependencia: el motor anota que esa derivación depende de esa fuente. Leer no es una operación pasiva; leer construye el grafo.

Escribir un valor de entrada. Cambia el valor y arranca la propagación. Escribir es el único punto donde entra información nueva al sistema.

Propagar. Recorrer el grafo desde el nodo escrito hacia los que dependen de él, decidiendo qué hay que recalcular y en qué orden. Aquí es donde los motores divergen radicalmente entre sí, y por eso le dedicaremos cuatro niveles enteros.

La estructura compartida sobre la que operan las tres es un grafo dirigido. Los nodos son valores —de entrada o derivados—; las aristas son dependencias. Cuando total lee precio, existe una arista de precio a total. Un motor de reactividad es, literalmente, ese grafo más el código que lo recorre.

flowchart LR
A[entrada precio] --> C[derivado subtotal]
B[entrada cantidad] --> C
C --> D[derivado total con iva]
D --> E[efecto escribir en el DOM]
style A fill:#89b4fa,color:#11111b
style B fill:#89b4fa,color:#11111b
style C fill:#cba6f7,color:#11111b
style D fill:#cba6f7,color:#11111b
style E fill:#a6e3a1,color:#11111b

Fíjate en la forma. A la izquierda, las raíces: nodos sin entradas, que solo cambian porque alguien los escribe. En el centro, las derivaciones puras. A la derecha, las hojas: nodos que no devuelven valor sino que producen un efecto en el mundo —escribir en el DOM, mandar una petición, imprimir un log. Esa forma de tres capas se repite en todos los motores que veremos, con nombres distintos.

Por qué es un problema de grafos

Podrías pensar que esto se resuelve con eventos: cada valor emite cuando cambia, cada interesado escucha. Es la intuición que casi todo el mundo tiene antes de mirar dentro de un motor real, y falla por tres razones concretas.

La primera es el orden. Si c depende de a y b, y b también depende de a, entonces al cambiar a hay que actualizar b antes que c. Un sistema de eventos no sabe eso; los manejadores corren en el orden en que se suscribieron. El resultado es que c puede evaluarse con el b viejo y el a nuevo, produciendo un valor que no corresponde a ningún estado real del sistema. Ese es el problema del rombo, y le dedicaremos un nivel entero.

La segunda es la redundancia. Con eventos, si c depende de a por dos caminos distintos, c se recalcula dos veces por cada cambio de a. Con n capas de derivación, el trabajo crece exponencialmente. Cualquiera que haya conectado a mano media docena de observadores conoce esa sensación de que el sistema hace mucho más trabajo del que debería.

La tercera es la pereza. Un valor derivado que nadie está mirando no debería recalcularse. Con eventos, el emisor no sabe si su valor le importa a alguien, así que calcula siempre. Un motor que conoce el grafo puede consultar quién observa a quién y saltarse ramas enteras.

Las tres razones apuntan a lo mismo: para decidir correctamente qué recalcular y en qué orden, necesitas conocer la topología completa de las dependencias, no solo los vecinos inmediatos. Y conocer la topología significa mantener un grafo explícito en memoria.

El motor es un planificador, no un notificador

Aquí está la reconceptualización que separa a quien usa un framework reactivo de quien entiende uno. Deja de pensar en la reactividad como en un sistema de mensajes que viajan y empieza a pensar en ella como en un planificador de tareas sobre un grafo de dependencias. El problema es idéntico al que resuelve un make o un bazel: dado un conjunto de artefactos derivados, unas entradas que han cambiado y un grafo de dependencias, decidir el subconjunto mínimo de reconstrucciones y el orden válido en que ejecutarlas. La única diferencia es la escala temporal: make corre una vez por invocación y puede permitirse leer el grafo entero del disco; un motor de reactividad corre miles de veces por segundo y tiene que mantener el grafo incrementalmente en memoria. Cuando en el nivel 5 hablemos de orden topológico y en el 8 de colas de efectos, reconocerás las mismas ideas que un sistema de build. Esta equivalencia no es una analogía didáctica: es literalmente el mismo problema, la construcción incremental, y quien la ve deja de sorprenderse de las decisiones de diseño de Solid o de Vue.

Lo que el motor no hace

Delimitar es tan útil como definir. Un motor de reactividad no decide cómo se pintan las cosas: eso lo hace la capa de renderizado que se apoya en él. No gestiona estado de servidor, caché de red ni reintentos: eso son librerías construidas encima. No impone inmutabilidad ni un flujo unidireccional: son disciplinas que puedes adoptar o no sobre cualquier motor.

Y sobre todo, no te dice de dónde viene el valor. Un signal no sabe si lo escribe un manejador de eventos, un setTimeout o un WebSocket. Al motor le da igual: recibe una escritura y propaga. Esa indiferencia es deliberada, y es lo que permite que el mismo motor sirva para una interfaz, para un sistema de build o para una hoja de cálculo.

La consecuencia práctica es que un motor de reactividad se puede estudiar —y se puede escribir— sin mencionar el DOM ni una sola vez. Eso es exactamente lo que haremos aquí: en el nivel 12 construiremos uno completo en unas ciento cuarenta líneas de JavaScript llano, y no habrá ni un document.createElement.

⚔️ Formula la invariante de un sistema que ya usas
  1. Coge una hoja de cálculo cualquiera. Identifica sus celdas de entrada y sus celdas con fórmula.
  2. Escribe la invariante del motor de recálculo de la hoja en una frase, con la misma forma que la de esta lección.
  3. Cambia una celda de entrada de la que dependen dos fórmulas encadenadas y razona en qué orden tuvo que evaluarlas la hoja.
  4. Explica por qué una hoja de cálculo puede permitirse recalcular al final de cada edición mientras que un motor de interfaz no.