El orden entre computaciones puras y efectos
Por qué todos los motores separan las derivaciones de los efectos en dos fases distintas, qué garantiza esa separación, y qué pasa cuando el orden entre efectos hermanos importa.
En un ciclo de propagación hay dos clases de trabajo con propiedades opuestas: las derivaciones son puras, perezosas y su orden lo impone la topología; los efectos son impuros, ansiosos y su orden lo decide una política. Confundirlas en una sola cola produce comportamientos que nadie quiere. Todos los motores acaban separándolas, y esta lección explica exactamente qué se gana con la separación.
- Justificar la separación entre computaciones puras y efectos.
- Enumerar las garantías que ofrece cada fase.
- Resolver el orden entre efectos hermanos sin depender de la cola.
- Reconocer las fases adicionales que introducen los frameworks.
Dos clases de trabajo
Las diferencias son sistemáticas y conviene tenerlas en una tabla.
| propiedad | derivación pura | efecto |
|---|---|---|
| produce | un valor que otros leen | un cambio fuera del sistema |
| se evalúa | cuando alguien la lee | siempre que se ensucia |
| pereza | perezosa | ansiosa |
| orden | lo impone la topología | lo decide una política |
| repetible | sí, sin consecuencias | no, cada ejecución tiene efectos |
| puede fallar a medias | se descarta y ya está | deja el mundo a medio cambiar |
La fila decisiva es la última. Una derivación que falla no ha hecho nada al mundo; se puede reintentar. Un efecto que falla a mitad ha escrito la mitad de lo que iba a escribir. Por eso los efectos merecen un tratamiento distinto y un momento de ejecución bien definido.
La separación en dos fases
El esquema que usan todos los motores es este.
Fase de derivaciones. Resolver todos los nodos puros marcados que hagan falta. Como son perezosos, en la práctica esto ocurre bajo demanda cuando un efecto los lee. La garantía es que ninguna derivación se evalúa antes que sus dependencias.
Fase de efectos. Vaciar la cola de efectos. Cada efecto, al ejecutarse, lee las derivaciones que necesita, y esas lecturas disparan la resolución perezosa de la fase anterior sobre la marcha.
Solid lo hace explícito con dos colas internas separadas: una para las computaciones puras y otra para los efectos, y vacía la primera antes que la segunda. La ventaja de tener la cola de puras es que se pueden resolver las derivaciones aunque nadie las lea todavía, lo que hace que la fase de efectos encuentre todo listo.
const Puras = [];
const Efectos = [];
function planificar(nodo) {
if (nodo.efecto) Efectos.push(nodo);
else Puras.push(nodo);
}
function vaciar() {
for (let i = 0; i < Puras.length; i++) actualizar(Puras[i]); // primero las derivaciones
Puras.length = 0;
for (let i = 0; i < Efectos.length; i++) actualizar(Efectos[i]); // luego los efectos
Efectos.length = 0;
}
flowchart TB W[escritura] --> M[marcado] M --> P[cola de derivaciones] M --> E[cola de efectos] P --> R1[resolver todas las derivaciones] R1 --> R2[ejecutar todos los efectos] E --> R2 R2 --> F[ciclo terminado] style W fill:#89b4fa,color:#11111b style M fill:#f9e2af,color:#11111b style P fill:#cba6f7,color:#11111b style E fill:#fab387,color:#11111b style R1 fill:#cba6f7,color:#11111b style R2 fill:#a6e3a1,color:#11111b style F fill:#a6e3a1,color:#11111b
Qué garantiza la separación
Ningún efecto ve una derivación a medio actualizar. Cuando empieza la fase de efectos, todo lo puro que había que resolver está resuelto. Es una garantía más fuerte que la simple ausencia de glitches: no solo cada valor es coherente, sino que el conjunto entero está estabilizado antes de que nada toque el mundo.
Los efectos no interfieren entre sí a través de derivaciones. Si el efecto A escribe algo que ensucia una derivación que el efecto B lee, esa derivación se resuelve cuando B la lea, no antes. El orden queda determinado.
Las derivaciones no se ejecutan de más. Si tres efectos leen el mismo memo sucio, se resuelve una vez en la primera lectura y las otras dos usan la caché.
Efectos hermanos y fases adicionales
El orden entre efectos hermanos
Queda el caso que ninguna garantía cubre: dos efectos que no dependen el uno del otro. Su orden relativo lo decide la política de la cola, y depender de él es frágil.
// FRAGIL: depende del orden de la cola
efecto(() => { medida = elemento.offsetHeight; });
efecto(() => { elemento.style.padding = `${relleno()}px`; });
Si el segundo corre antes que el primero, la medición se hace después del cambio de relleno y da otro resultado. Y el orden depende de cuál se creó antes, que es un detalle de la estructura del código.
La solución robusta es hacer la dependencia explícita en el grafo.
// ROBUSTO: el orden lo impone la topologia
const relleno = senal(8);
const rellenoAplicado = memo(() => { elemento.style.padding = `${relleno()}px`; return relleno(); });
efecto(() => { rellenoAplicado(); medida = elemento.offsetHeight; });
Aunque el ejemplo mete un efecto secundario en un memo, que la lección 4 del nivel 6 desaconseja. La forma limpia es una señal intermedia.
const aplicado = senal(0);
efecto(() => { elemento.style.padding = `${relleno()}px`; aplicado.set(n => n + 1); });
efecto(() => { aplicado(); medida = elemento.offsetHeight; });
Ahora el segundo efecto depende del primero y el orden está garantizado por el grafo, no por la cola.
Es la regla general y no admite muchas excepciones. Un motor reactivo garantiza el orden entre nodos conectados y no promete nada entre nodos independientes. Si tu código necesita un orden, esa necesidad es una dependencia, y las dependencias van en el grafo. Escribirla como una dependencia además documenta la restricción, que es una ventaja secundaria nada despreciable.
Las fases adicionales de los frameworks
Un framework construido sobre el motor suele añadir más fases, porque el pintado del navegador es un evento con el que hay que coordinarse.
Un esquema típico tiene cuatro momentos: resolver derivaciones, ejecutar los efectos que preparan —los que escriben en el DOM—, dejar que el navegador pinte, y ejecutar los efectos que miden o reaccionan al resultado.
Vue expone exactamente eso con las opciones pre, post y sync de sus watchers. Angular tiene afterRenderEffect para el trabajo posterior al renderizado. Svelte tiene $effect.pre para el trabajo anterior a la actualización del DOM. Solid tiene createRenderEffect, que corre en la fase de renderizado, frente a createEffect, que corre después.
La razón es siempre la misma: leer geometría del DOM después de escribir en él fuerza un recálculo de estilo síncrono, y agrupar todas las escrituras antes de todas las lecturas evita ese coste. Es la misma disciplina de separar lecturas y escrituras del DOM que ya conoces, aplicada al ciclo del motor.
La separación de estas dos fases refleja algo más profundo que una optimización de planificación: es la frontera del sistema. Dentro de la fase de derivaciones, todo es puro, reordenable, repetible y descartable; el motor puede evaluar en el orden que quiera, saltarse lo que no haga falta y repetir sin consecuencias. En cuanto empieza la fase de efectos, cada operación es irreversible: se ha escrito en el DOM, se ha lanzado una petición, se ha guardado algo. Esa asimetría es la que justifica que las dos fases estén separadas y que la impura vaya siempre después. Reconocerlo tiene una consecuencia de diseño muy concreta para el código que escribas: cuanto más trabajo consigas mover a la fase pura, más margen le das al motor para optimizarlo y más fácil te resulta razonar sobre él. Un cálculo dentro de un efecto es opaco, se ejecuta siempre y no se puede compartir; el mismo cálculo en un memo es perezoso, cacheado, compartible entre consumidores y capaz de cortar cascadas. La regla que sale de aquí es limpia y merece hacerse costumbre: los efectos deben ser lo más finos posible y contener solo la operación irreversible; todo lo que sea decidir, formatear, filtrar o combinar pertenece a la fase pura. Aplicada con disciplina, esta regla sola mejora tanto el rendimiento como la depurabilidad de un sistema reactivo más que cualquier ajuste de memoización.
- Escribe dos efectos independientes donde el orden sea observable y comprueba que depende del orden de creación.
- Invierte el orden de creación y verifica que el resultado cambia.
- Introduce una señal intermedia que haga explícita la dependencia y comprueba que el orden ya no depende de la creación.
- Mueve todo el cálculo de uno de los efectos a un memo y mide cuántas veces se ejecuta cada parte antes y después.