Saltar al contenido

Anatomía del output: template, clonar, insert y effect

La disección línea por línea del JavaScript que el compilador emite para un componente: la plantilla izada al módulo, el clon del nodo con `cloneNode`, la navegación estática por `firstChild` y `nextSibling`, el `insert()` que crea un efecto de render para el contenido vivo, y el `effect()` agrupado con el objeto de valores previos `_p$` que parchea varios atributos con una sola suscripción y guardas de igualdad. Cinco piezas que convierten el output compilado en un mapa exacto de dónde y por qué reacciona tu interfaz.

La plantilla se iza y se clona

Partimos de una tarjeta con dos zonas fijas —el contenedor y un título— y dos zonas vivas —una clase condicional y el texto de un span—.

TSX
function Tarjeta(props: { nombre: () => string; activo: () => boolean }) {  return (    <div class="tarjeta" classList={{ activa: props.activo() }}>      <h2>Perfil</h2>      <span>{props.nombre()}</span>    </div>  );}

Lo primero que aparece en el output es una constante al nivel del módulo, fuera de la función. template() corre una sola vez al cargar el módulo: crea un elemento template del navegador, parsea el HTML y guarda su subárbol ya construido.

JS
import { template as _$template } from "solid-js/web";const _tmpl$ = _$template(`<div class=tarjeta><h2>Perfil</h2><span>`);

Todo el esqueleto estático —el div, su clase, el h2 con su texto y el span vacío— viaja en esa única cadena. Cada llamada posterior a _tmpl$() devuelve un cloneNode(true) profundo de ese subárbol ya parseado, que es la forma más rápida de fabricar DOM que existe. Una sola plantilla, compartida por todas las instancias de Tarjeta y por cada recreación. El compilador ya sabía, antes de que existiera el navegador, qué era constante y qué dependía del grafo; esa frontera decide todo lo que viene después.

Clonar, navegar, ligar

El cuerpo del componente compila a una IIFE que clona el esqueleto, camina hasta los nodos vivos por punteros fijos y engancha ahí la reactividad.

JS
import { insert as _$insert, effect as _$effect, classList as _$classList } from "solid-js/web";const Tarjeta = (props) => {  return (() => {    const _el$ = _tmpl$();               // clona el esqueleto    const _el$2 = _el$.firstChild;       // el <h2>    const _el$3 = _el$2.nextSibling;     // el <span>    _$insert(_el$3, () => props.nombre());  // contenido vivo del span    _$effect(() => _$classList(_el$, { activa: props.activo() }));    return _el$;  })();};

La navegación por firstChild y nextSibling no es casual: el compilador calcula en tiempo de build una ruta estática de punteros hasta cada nodo dinámico, sin querySelector, sin id, sin búsquedas. Son saltos de puntero, deterministas y baratos, resueltos antes de que el código corra. Si añadieras un nodo estático entre el h2 y el span, el compilador recalcularía la ruta —un nextSibling más— pero el coste en tiempo de ejecución seguiría siendo el mismo: caminar el esqueleto una vez al crear la instancia.

Cuando el contenido dinámico cae entre dos zonas fijas, el compilador no puede insertarlo al final: crea un nodo marcador y se lo pasa a insert como tercer argumento para fijar la posición exacta.

TSX
<p>Hola {nombre()}, tienes {n()} mensajes</p>
JS
// dos huecos vivos entre texto fijo, cada insert con su referenciaconst _el$2 = _el$.firstChild.nextSibling;   // nodo marcador tras "Hola "_$insert(_el$, nombre, _el$2);   // inserta ANTES de _el$2, no al final_$insert(_el$, n, null);         // este si va al final

El marcador es la respuesta del compilador a un problema puramente posicional: mantener cada trozo dinámico en su sitio aunque el que tiene delante crezca o encoja.

insert y effect: el contenido y los atributos

insert(padre, accessor, marcador) es la pieza que da vida al contenido. Cuando el segundo argumento es una función, insert la envuelve internamente en un createRenderEffect: cualquier signal que se lea dentro re-ejecuta solo esa inserción. Actualiza el nodo de texto en el sitio si el valor sigue siendo texto, o reemplaza nodos si cambia su forma. El tercer argumento —el marcador, aquí ausente— indica ante qué hermano insertar. Esta es la actualización quirúrgica: cambiar nombre despierta solo este efecto y toca solo el nodo de texto de este span, sin rozar el div que lo contiene.

Ese envoltorio interno es literal. insert mira el tipo de lo que recibe y, si es una función, la mete en un efecto de render propio; si es un valor plano, lo escribe una vez y termina. Ahí está, en una condición, la diferencia entre reaccionar y no reaccionar.

JS
// esbozo de lo que insert hace por dentrofunction insert(padre, valor, marcador) {  if (typeof valor === "function")    createRenderEffect(() => reconciliar(padre, valor(), marcador));  else reconciliar(padre, valor, marcador);   // dato plano: una sola vez}

El otro extremo son los atributos. Cuando un elemento tiene varios reactivos, el compilador no crea un efecto por cada uno: los agrupa en un solo efecto con un objeto de valores previos que evita escrituras inútiles.

TSX
<a href={url()} class={clase()} title={titulo()}>ir</a>
JS
_$effect((_p$) => {  const _v$ = url(), _v$2 = clase(), _v$3 = titulo();  _v$ !== _p$.e && _$setAttribute(_el$, "href", _p$.e = _v$);  _v$2 !== _p$.t && _$className(_el$, _p$.t = _v$2);  _v$3 !== _p$.a && _$setAttribute(_el$, "title", _p$.a = _v$3);  return _p$;}, { e: undefined, t: undefined, a: undefined });

Un único efecto de render lee los tres accessors, de modo que se re-ejecuta si cualquiera cambia; pero el objeto _p$ cachea el último valor de cada uno y las guardas !== garantizan que la escritura al DOM ocurra solo para el atributo que de verdad cambió. Una suscripción, tres escrituras vigiladas. Por eso un elemento con varios atributos vivos no genera varios efectos: genera uno, barato de suscribir y preciso al aplicar.

Fíjate además en que cada tipo de atributo tiene su propio helper. El compilador ya sabe, en build, cuál corresponde a cada binding, así que en runtime no queda ninguna decisión que tomar: className toca la propiedad class de un paso, style fusiona el objeto de estilos y setProperty escribe en la propiedad del nodo, no en el atributo.

JS
_$className(_el$, clase());         // ruta directa a class_$style(_el$, estilos());           // fusiona el objeto de estilos_$setProperty(_el$, "value", v());  // propiedad, no atributo