Quién posee a quién
El segundo hueco global del motor: el dueño actual. Cómo se forma el árbol de propiedad al ejecutar, qué se registra en él, y por qué es un árbol y no un grafo.
Hay una segunda variable de módulo en cualquier motor de esta familia, y es tan importante como el observador actual: el dueño actual. Mientras el observador construye el grafo de dependencias, el dueño construye un árbol de propiedad completamente distinto que responde a otra pregunta: cuando esto desaparezca, qué hay que destruir con ello. Son dos estructuras superpuestas sobre los mismos nodos, y confundirlas es el origen de la mitad de los malentendidos sobre este mecanismo.
- Implementar el dueño actual y el registro de hijos.
- Distinguir el árbol de propiedad del grafo de dependencias.
- Explicar por qué la propiedad es un árbol y las dependencias un grafo.
- Reconocer qué se registra en el dueño además de las computaciones.
La segunda variable
El mecanismo es idéntico al del observador actual: una variable de módulo, guardar, asignar, ejecutar, restaurar.
let Duenno = null;
function registrar(nodo) {
if (Duenno) Duenno.hijos.push(nodo);
}
function alLimpiar(fn) {
if (Duenno) Duenno.limpiezas.push(fn);
}
Y en la ejecución de una computación se asignan las dos variables a la vez, porque una computación es a la vez el observador de lo que lee y el dueño de lo que crea.
function ejecutar(nodo) {
limpiar(nodo); // destruir lo que creo la pasada anterior
desatar(nodo); // destruir las aristas de la pasada anterior
const obsAnterior = Observador, duennoAnterior = Duenno;
Observador = nodo; // lo que lea, lo lee este nodo
Duenno = nodo; // lo que cree, lo posee este nodo
try {
nodo.valor = nodo.fn(nodo.valor);
} finally {
Observador = obsAnterior;
Duenno = duennoAnterior;
nodo.estado = LIMPIO;
}
}
Que se asignen juntas hace pensar que son lo mismo. No lo son, y la lección se dedica a separarlas.
Dos estructuras sobre los mismos nodos
El grafo de dependencias conecta un nodo con los valores que lee. Es un grafo dirigido acíclico: un nodo puede tener muchas fuentes y muchos observadores. Responde a qué hay que recalcular cuando esto cambie.
El árbol de propiedad conecta un nodo con el ámbito en el que se creó. Es un árbol: cada nodo tiene exactamente un padre. Responde a qué hay que destruir cuando esto desaparezca.
Las dos relaciones son independientes. Un memo creado dentro del efecto A puede leer una señal creada en el efecto B: su dueño es A y su fuente es B. Nada obliga a que coincidan.
const global = senal(0); // duenno: la raiz
efecto(() => { // duenno: la raiz
const local = memo(() => global() * 2); // duenno: este efecto, fuente: global
usar(local());
});
flowchart TB subgraph propiedad R[raiz] --> E[efecto] E --> M[memo local] end subgraph dependencias S[senal global] --> M2[memo local] M2 --> E2[efecto] end style R fill:#94e2d5,color:#11111b style E fill:#94e2d5,color:#11111b style M fill:#94e2d5,color:#11111b style S fill:#89b4fa,color:#11111b style M2 fill:#cba6f7,color:#11111b style E2 fill:#a6e3a1,color:#11111b
Fíjate en que las flechas van en direcciones distintas entre los mismos nodos. En propiedad, el efecto es padre del memo. En dependencias, el memo es fuente del efecto. No es una contradicción: son dos relaciones distintas.
Por qué la propiedad es un árbol
La razón es de definición y merece verla: un nodo se crea en un único momento, y en ese momento hay un único dueño actual. No hay forma de que un nodo tenga dos padres, porque no puede crearse dos veces.
Que sea un árbol es lo que hace posible la limpieza en cascada. Si fuera un grafo, destruir un nodo plantearía la pregunta de si sus hijos deben morir cuando muere uno de sus padres o cuando mueren todos, que es exactamente el problema del conteo de referencias con sus ciclos y sus ambigüedades. Con un árbol la respuesta es única y trivial: mueren con su único padre.
Es la misma razón por la que los sistemas de gestión de recursos —desde los destructores encadenados hasta los ámbitos estructurados de la concurrencia moderna— eligen siempre una estructura de árbol. La propiedad única es lo que hace la destrucción determinista.
Contenido del dueño y vocabulario
Qué se registra en el dueño
Tres cosas distintas, y conviene tenerlas separadas.
Computaciones hijas. Los memos y efectos creados durante la ejecución. Se desecharán en cascada.
Callbacks de limpieza. Funciones registradas con la primitiva de limpieza, que se ejecutan al desechar el dueño. Aquí es donde se cancelan temporizadores, se quitan escuchadores de eventos y se abortan peticiones.
Contexto. Muchos motores usan el árbol de propiedad como canal para pasar valores hacia abajo sin pasarlos por parámetros, subiendo por el árbol hasta encontrar quien lo proporcione. Es el mismo mecanismo que un contexto de React, implementado sobre esta estructura en vez de sobre el árbol de componentes.
const ambito = {
hijos: [], // computaciones y subambitos creados aqui dentro
limpiezas: [], // callbacks a ejecutar al desechar
contexto: null, // valores heredados por los descendientes
};
En un framework construido sobre este motor, cada componente crea un ámbito, así que el árbol de propiedad acaba siendo isomorfo al árbol de componentes. Esa coincidencia es útil —desmontar un componente desecha su ámbito— pero no es una necesidad del motor: el árbol de propiedad existe con o sin componentes, y en una aplicación sin interfaz sigue teniendo sentido.
Aquí está la razón de fondo por la que este mecanismo existe, y es más profunda que la comodidad. JavaScript tiene recolección de basura, lo que resuelve la memoria y no resuelve los recursos. Un temporizador activo, un escuchador de eventos registrado, una conexión abierta, una petición en vuelo: ninguno se libera porque el objeto que lo creó deje de estar referenciado. Hacen falta acciones explícitas: clearInterval, removeEventListener, abort. En un lenguaje con destructores deterministas eso se resuelve con la propiedad y el ámbito; en JavaScript no hay tal cosa, y FinalizationRegistry es explícitamente no determinista y no sirve para esto. El árbol de dueños es, exactamente, la reintroducción de la propiedad determinista sobre un lenguaje que la había eliminado. Y la razón por la que un motor reactivo la necesita más que otras librerías es que un motor reactivo crea recursos implícitamente: tú escribes un efecto, y sin darte cuenta has creado una suscripción que hay que dar de baja. Si cada suscripción implícita exigiera una baja explícita, el modelo sería inutilizable —volveríamos a la contabilidad manual de suscripciones que hacía insoportable la programación con observables—. El dueño hace que la creación implícita tenga una destrucción implícita a juego, y esa simetría es lo que hace el modelo usable a escala. Cuando lo veas así, entenderás por qué Solid, Vue con sus ámbitos de efecto y Angular con su inyector convergieron los tres en el mismo mecanismo: no es una elección de diseño, es lo que hace falta para que la creación implícita de recursos no acabe en desastre.
Cómo se llama en cada motor
Solid lo llama Owner y expone createRoot, onCleanup, getOwner y runWithOwner. Vue lo llama effectScope, con getCurrentScope, onScopeDispose y la opción de crear un ámbito desacoplado. Angular lo ata al inyector: un effect() creado en un contexto de inyección se destruye con él, y también admite un DestroyRef para registrar limpiezas. Svelte lo tiene en $effect.root y en la limpieza que devuelve un $effect. Preact Signals no lo tiene: effect devuelve una función de baja y la responsabilidad es enteramente del programador.
Esa última ausencia es interesante y no es un descuido: Preact Signals está pensado para usarse dentro de un framework que ya gestiona ciclos de vida, así que delega. Es una decisión de alcance, y el nivel 10 la revisita.
- Instrumenta tu motor para volcar por separado el árbol de propiedad y el grafo de dependencias.
- Crea un memo dentro de un efecto que lea una señal creada fuera, y dibuja las dos estructuras.
- Comprueba que las aristas de una no coinciden con las de la otra.
- Encuentra un caso donde un nodo tenga varias fuentes y exactamente un dueño, y explica por qué eso siempre es así.