Angular: versiones, épocas y el inyector como dueño
Las decisiones de Angular: propagación basada en contadores de versión y una época global, ciclo de vida delegado al inyector de dependencias, y la sustitución del parcheo global de la asincronía por un grafo reactivo.
Angular llegó a las señales desde un punto de partida distinto al de los demás: no tenía un problema de granularidad sino de detección de cambios. Su mecanismo anterior parcheaba las APIs asíncronas del navegador para saber cuándo algo podía haber cambiado, y luego comprobaba el árbol entero. Las señales le dieron la información precisa que le faltaba, y la migración a un modo sin zonas es uno de los cambios arquitectónicos más grandes que ha hecho un framework grande en producción.
- Situar las decisiones de Angular en las ocho dimensiones del track.
- Entender la propagación por contadores de versión y época global.
- Explicar por qué el inyector hace de árbol de dueños.
- Reconocer qué cambió al eliminar el parcheo global de la asincronía.
Las ocho decisiones
Representación del grafo. Nodos reactivos con listas de productores y consumidores, más contadores de versión por nodo. La comprobación de si algo cambió se hace comparando versiones y no valores.
Descubrimiento. Tracking automático con un consumidor activo. untracked para leer sin rastrear.
Propagación. Push-pull con notificación hacia adelante y verificación de versiones hacia atrás, reforzada por una época global que evita verificar dos veces en el mismo ciclo. Es la combinación de las tres técnicas de la lección 5 del nivel 5.
Consistencia. Glitch-free por la resolución hacia arriba antes de leer.
Memoización. computed, con opción equal para la función de igualdad.
Ciclo de vida. Delegado al inyector de dependencias: un effect() creado en un contexto de inyección se destruye cuando ese contexto se destruye. También admite un DestroyRef explícito para casos fuera de ese contexto.
Planificación. Asíncrona, a través del planificador de detección de cambios. afterRenderEffect para el trabajo posterior al renderizado.
Modelo de estado. Solo atómico. signal(), con .set() y .update(). El estado profundo se modela con señales anidadas o con estructuras que se reemplazan.
Versiones y época
Angular es el motor que más apoya su propagación en contadores, y merece verlo porque es una alternativa legítima al marcado de tres estados.
La idea: cada productor lleva una versión que se incrementa en cada cambio efectivo de valor. Cada consumidor guarda las versiones que vio de cada productor. Antes de devolver un valor cacheado, el consumidor comprueba si alguna versión avanzó; si ninguna lo hizo, el valor sigue vigente.
// Esquema del mecanismo, simplificado
function asegurarActualizado(nodo) {
if (nodo.epocaVerificada === EpocaGlobal) return; // ya comprobado este ciclo
nodo.epocaVerificada = EpocaGlobal;
for (const p of nodo.productores) {
asegurarActualizado(p);
if (p.version !== nodo.versionesVistas.get(p)) { recalcular(nodo); return; }
}
}
La ventaja sobre comparar valores es la del nivel 5: comparar enteros es barato y no hace falta conservar el valor anterior. La época global evita el coste cuadrático en grafos con rombos anidados.
Las primitivas fundamentales —signal, computed, effect, linkedSignal, y las consultas y entradas basadas en señales— alcanzaron estabilidad en la versión 20. Antes de eso convivieron un tiempo en vista previa, lo que explica que haya código y tutoriales con APIs que ya no existen; mutate, por ejemplo, se eliminó antes de la estabilización.
El inyector como árbol de dueños
Angular no construyó un árbol de dueños propio porque ya tenía uno: la jerarquía de inyectores de dependencias, que sabe con precisión cuándo se destruye cada componente, cada directiva y cada servicio.
@Component({ /* ... */ })
export class Panel {
datos = signal<Fila[]>([]);
constructor() {
effect(() => {
// creado en el contexto de inyeccion del componente:
// se destruye cuando el componente se destruye
guardar(this.datos());
});
}
}
Reutilizar una estructura existente en vez de crear una paralela es una decisión de diseño sólida y tiene una ventaja concreta: el ciclo de vida de los efectos coincide exactamente con el de todo lo demás en el framework, sin dos sistemas que puedan desincronizarse.
El precio es que crear un efecto fuera de un contexto de inyección exige pasar el inyector explícitamente, lo cual es más ceremonioso que una raíz desacoplada. Es un intercambio consistente con el resto de Angular, donde la inyección es el mecanismo central de composición.
Lo que sustituyó y lo que añadió
De las zonas al grafo
El cambio más profundo no es la API sino lo que sustituye. El mecanismo anterior de Angular parcheaba las APIs asíncronas del navegador —temporizadores, eventos, peticiones— para enterarse de que había ocurrido algo que podía haber cambiado el estado, y a continuación recorría el árbol de componentes comprobando si algo había cambiado de verdad.
Ese diseño resolvía el problema del descubrimiento sin exigir nada al programador, y pagaba dos precios: un recorrido del árbol por cada evento asíncrono, y una librería de parcheo que había que cargar y mantener.
Con señales, la información llega exacta: el motor sabe qué componentes dependen de qué señal, así que no hay que comprobar nada. El resultado es la detección de cambios sin zonas, estable desde la versión 20.2, y desde la versión 21 las aplicaciones nuevas ya no incluyen zone.js por defecto.
flowchart TB A[modelo con zonas] --> A1[parchear las apis asincronas] A1 --> A2[algo ocurrio, comprobar el arbol entero] A2 --> A3[coste proporcional al numero de componentes] B[modelo con senales] --> B1[la escritura notifica a sus dependientes] B1 --> B2[marcar solo los componentes afectados] B2 --> B3[coste proporcional a lo que cambio] style A fill:#f9e2af,color:#11111b style A1 fill:#f9e2af,color:#11111b style A2 fill:#f38ba8,color:#11111b style A3 fill:#f38ba8,color:#11111b style B fill:#89b4fa,color:#11111b style B1 fill:#89b4fa,color:#11111b style B2 fill:#a6e3a1,color:#11111b style B3 fill:#a6e3a1,color:#11111b
linkedSignal y el estado derivado sobrescribible
Angular tiene una primitiva que los demás no ofrecen con nombre propio y que resuelve un patrón muy real: el estado que normalmente se deriva de otra cosa pero que el usuario puede sobrescribir, y que se resetea cuando la fuente cambia.
categoria = signal('libros');
// se resetea al cambiar de categoria, pero el usuario puede elegir otro
seleccionado = linkedSignal(() => primerProductoDe(this.categoria()));
Es exactamente el caso legítimo de la lección 5 del nivel 8: un efecto que escribe, pero que no lee lo que escribe, y por tanto converge en una sola ronda. Tener una primitiva con nombre para ese patrón evita que la gente lo implemente a mano con un efecto y se encuentre con las cascadas.
Lo más instructivo de Angular para quien estudia motores no es su implementación sino lo que su historia demuestra. Angular tenía un modelo de componentes maduro, un sistema de inyección de dependencias, plantillas compiladas y un ecosistema enorme construido sobre RxJS, y cambió su motor de reactividad sin cambiar nada de eso. Eso prueba una tesis que atraviesa todo este track: el motor de reactividad es una capa separable. No está atado al modelo de componentes, ni al sistema de plantillas, ni al de módulos. Es la maquinaria que decide qué recalcular, y se puede sustituir por otra siempre que ofrezca las mismas garantías. La consecuencia práctica es doble. Por un lado, aprender un motor a fondo —que es lo que estás haciendo— transfiere entre frameworks casi sin pérdida, porque lo que aprendes es la capa que se comparte. Por otro, y más interesante de cara al futuro, si el motor es separable entonces puede estandarizarse, que es exactamente la apuesta de la propuesta de Signals en TC39. Angular no es solo un motor más en la comparación: es la prueba de existencia de que la sustitución es posible en un sistema real y grande, y esa prueba es un argumento serio a favor de que la estandarización tiene sentido.
- Implementa la propagación por versiones con época global sobre un motor propio.
- Construye un grafo con cuatro rombos anidados y cuenta las verificaciones con y sin la época.
- Compara ese número con el que produce el marcado de tres estados sobre el mismo grafo.
- Añade un caso donde el valor vuelva a su valor original y comprueba en qué se diferencian las dos técnicas.