El mòdul 5 ha tancat amb una pregunta pendent: cada vegada que estado canvia a <task-card> o tareas canvia a <task-board>, alguna cosa passa internament a Lit perquè la pantalla s'actualitzi, però aquest curs mai s'ha aturat a explicar aquest "alguna cosa" amb precisió. Abans d'entrar en els hooks propis de Lit (contingut de la lliçó següent), convé fixar primer una base més elemental: els callbacks del cicle de vida que Lit hereta directament de l'estàndard de Custom Elements, sense afegir-hi res propi, i que ja s'han esmentat de passada al mòdul 1 sense desenvolupar-los. Aquesta lliçó els explica en detall i els aplica a un problema real de TaskFlow: avisar visualment quan una tasca està a punt d'arribar a la seva fechaLimite, mitjançant un temporitzador que cal crear i destruir en el moment correcte.
Contingut
- Custom Elements ja tenia un cicle de vida abans que existís Lit
connectedCallback: quan entra un element al DOMdisconnectedCallback: quan surt, i per què importa netejar- Per què (gairebé) mai cal sobreescriure el
constructor - Taula comparativa: constructor, connectedCallback i disconnectedCallback
- Cas real: avisar quan una tasca està a prop de la seva data límit
- Tancament: què queda per explicar
- Custom Elements ja tenia un cicle de vida abans que existís Lit
Tot el que s'explica en aquesta lliçó no és una característica de Lit: forma part de l'especificació estàndard de Custom Elements, la mateixa API del navegador sobre la qual Lit es construeix, ja esmentada a la primera lliçó del curs. Qualsevol classe que estengui HTMLElement (amb o sense Lit pel mig) pot declarar fins a quatre mètodes especials, amb noms reservats, que el navegador crida automàticament en moments concrets de la vida d'un element personalitzat: constructor, connectedCallback, disconnectedCallback i attributeChangedCallback (aquest últim, gestionat gairebé sempre internament per Lit per sincronitzar atributs i propietats, com es va veure al mòdul 3, i rares vegades cal tocar-lo a mà).
Aquesta lliçó se centra en els tres primers. Són "callbacks" en el sentit més literal: funcions que el propi navegador invoca pel seu compte, en resposta al fet que l'element es creï, s'insereixi en un document o se n'elimini; el desenvolupador no els crida mai directament, només els sobreescriu per afegir-hi codi que s'hagi d'executar en aquests moments concrets.
connectedCallback: quan entra un element al DOM
connectedCallback: quan entra un element al DOMconnectedCallback s'executa cada vegada que l'element s'insereix en un document amb capacitat de renderitzat (normalment, el DOM de la pàgina visible al navegador). La paraula "cada vegada" és deliberada: no és un esdeveniment que passi una única vegada en la vida d'un element, sinó potencialment diverses, perquè un element es pot treure del DOM i tornar-s'hi a inserir més endavant (per exemple, si algun codi de l'aplicació mou un node d'un contenidor a un altre amb appendChild), i cada una d'aquestes insercions torna a disparar connectedCallback.
class TaskCard extends LitElement {
connectedCallback() {
super.connectedCallback();
console.log('task-card insertada en el DOM');
}
}El primer detall que mereix atenció és la crida a super.connectedCallback(). LitElement ja té la seva pròpia implementació de connectedCallback, que fa una feina interna imprescindible (entre altres coses, programa la primera actualització del component si encara no s'ha renderitzat). Sobreescriure connectedCallback sense cridar primer super.connectedCallback() trencaria aquesta feina interna, així que la convenció, sense excepció, és cridar sempre super.connectedCallback() com a primera línia del mètode, abans d'afegir-hi cap codi propi.
connectedCallback és el lloc recomanat per a qualsevol inicialització que depengui del fet que l'element estigui realment connectat a un document: arrencar temporitzadors, afegir listeners a objectes externs al propi component (per exemple, a window o a document), obrir una connexió, o qualsevol altre efecte que només tingui sentit mentre el component estigui visible i actiu.
disconnectedCallback: quan surt, i per què importa netejar
disconnectedCallback: quan surt, i per què importa netejardisconnectedCallback és el complement exacte de connectedCallback: s'executa cada vegada que l'element es retira d'un document amb capacitat de renderitzat, ja sigui perquè s'elimina definitivament o perquè, com s'ha assenyalat a l'apartat anterior, es mou d'un lloc a un altre del DOM (una operació de moviment es tradueix, internament, en una desconnexió seguida d'una reconnexió).
class TaskCard extends LitElement {
disconnectedCallback() {
super.disconnectedCallback();
console.log('task-card retirada del DOM');
}
}La raó per la qual aquest callback importa tant a la pràctica és la neteja de recursos: tot el que s'activi a connectedCallback i no depengui exclusivament del propi cicle de vida de Lit s'ha de desactivar explícitament a disconnectedCallback. Un temporitzador arrencat amb setInterval a connectedCallback i mai aturat continuarà executant-se indefinidament, fins i tot després que l'element hagi desaparegut del DOM i, en aparença, "ja no existeixi"; mentre l'interval continuï actiu, el motor de JavaScript manté viva una referència a l'objecte (a través del tancament de la funció que s'executa a cada tick), i aquest objecte no es pot alliberar de memòria. Això és una fuga de memòria en el sentit més clàssic del terme, i és exactament el tipus d'error que disconnectedCallback existeix per evitar.
Igual que amb connectedCallback, la convenció és cridar sempre super.disconnectedCallback(), per no interferir amb la neteja interna que LitElement fa pel seu compte.
- Per què (gairebé) mai cal sobreescriure el
constructor
constructorQui arriba a Lit des d'altres llenguatges o frameworks orientats a objectes tendeix a usar el constructor com el lloc natural per inicialitzar qualsevol cosa. A Lit, però, el constructor té dues limitacions importants que fan que, en la immensa majoria dels casos, no sigui el lloc adequat:
- L'element encara no està connectat al DOM quan s'executa el
constructor. Un element personalitzat es pot crear (per exemple, ambdocument.createElement('task-card')) molt abans d'inserir-se en cap lloc, o fins i tot sense arribar a inserir-se mai. Qualsevol inicialització que depengui del fet que l'element estigui realment visible en una pàgina —com el temporitzador d'aquesta lliçó— arrencaria en el moment equivocat si visqués alconstructor: podria arrencar per a un element que mai arriba a mostrar-se, i no existeix capconstructorsimètric que s'executi en destruir l'objecte per poder-lo netejar (a diferència dedisconnectedCallback, que sí que existeix amb aquest propòsit). - Els valors per defecte de les propietats reactives ja cobreixen la inicialització simple. Tots els exemples de TaskFlow des del mòdul 3 inicialitzen
titulo,estado,prioridadofechaLimitedins delconstructorúnicament perquè aquí és on, per convenció de JavaScript, s'assignen valors a camps d'instància abans que la classe estigui llesta; però no hi ha cap lògica de cicle de vida en joc en aquestes assignacions, només valors inicials plans. Quan la inicialització és així de simple (this.estado = 'pendiente'), elconstructorcontinua sent perfectament adequat; el problema apareix quan aquesta inicialització implica efectes actius —temporitzadors, subscripcions, listeners externs—, perquè aleshores sí que cal esperar aconnectedCallback.
La regla pràctica que se segueix a TaskFlow, i que resumeix bé el criteri general de Lit, és aquesta: el constructor inicialitza valors; connectedCallback inicia efectes. Si una propietat només necessita un valor inicial raonable, continua anant al constructor. Si una operació implica alguna cosa que continua "viva" mentre el component estigui en pantalla (i que cal apagar quan deixi d'estar-ho), pertany a la parella connectedCallback/disconnectedCallback, mai al constructor en solitari.
- Taula comparativa: constructor, connectedCallback i disconnectedCallback
| Callback | Quan s'executa? | Quantes vegades? | L'element està al DOM? | Ús típic |
|---|---|---|---|---|
constructor |
En crear la instància de la classe | Una sola vegada en tota la vida de l'objecte | No necessàriament | Assignar valors inicials a camps i propietats |
connectedCallback |
En inserir-se en un document renderitzable | Una o més vegades (cada inserció, incloses les reinsercions) | Sí | Arrencar temporitzadors, subscripcions o listeners externs |
disconnectedCallback |
En retirar-se d'un document renderitzable | Una vegada per cada connectedCallback corresponent |
Ja no (s'acaba de retirar) | Aturar i alliberar tot el que es va activar a connectedCallback |
Aquesta taula deixa veure un principi de simetria que convé interioritzar: qualsevol cosa que s'activi dins de connectedCallback hauria de tenir la seva contrapartida exacta, desactivant-la, dins de disconnectedCallback. És la mateixa disciplina de "qui obre, tanca" que apareix en molts altres contextos de programació (obrir i tancar un fitxer, adquirir i alliberar un bloqueig), aplicada aquí al cicle de vida d'un component.
- Cas real: avisar quan una tasca està a prop de la seva data límit
TaskFlow ja té, des del mòdul 3, una propietat fechaLimite a <task-card>, convertida en un objecte Date real gràcies al conversor personalitzat vist en aquell moment. Fins ara, aquesta data només es mostrava com a text; aquesta lliçó l'aprofita per a alguna cosa més útil: ressaltar visualment la targeta quan falta menys d'un dia perquè es compleixi.
Com que el pas del temps no depèn del fet que cap propietat reactiva canviï (una tasca pot passar de "lluny de vèncer" a "a prop de vèncer" sense que ningú modifiqui fechaLimite ni cap altra propietat; simplement passen els minuts), no n'hi ha prou de recalcular aquesta situació quan canviï alguna dada: cal comprovar-ho periòdicament amb un temporitzador, i aquest temporitzador és exactament el tipus d'"efecte actiu" que pertany a connectedCallback/disconnectedCallback, no al constructor.
// src/components/task-card.js
class TaskCard extends LitElement {
static properties = {
titulo: { type: String },
estado: { type: String },
prioridad: { type: Number },
urgente: { type: Boolean },
expandida: { state: true },
fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
cercaDeVencer: { state: true },
};
constructor() {
super();
this.titulo = '';
this.estado = 'pendiente';
this.prioridad = 1;
this.urgente = false;
this.expandida = false;
this.fechaLimite = null;
this.cercaDeVencer = false;
}
connectedCallback() {
super.connectedCallback();
// Comprova immediatament, i a partir d'aquí cada minut,
// si la tasca ha entrat en la finestra de "a prop de vèncer".
this.cercaDeVencer = this._calcularSiCercaDeVencer();
this._idIntervalo = setInterval(() => {
this.cercaDeVencer = this._calcularSiCercaDeVencer();
}, 60000);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this._idIntervalo);
}
_calcularSiCercaDeVencer() {
if (!this.fechaLimite) {
return false;
}
const unDiaEnMs = 24 * 60 * 60 * 1000;
const msRestantes = this.fechaLimite.getTime() - Date.now();
return msRestantes > 0 && msRestantes <= unDiaEnMs;
}
render() {
return html`
<article @click="${this.alternarExpandida}">
<h3>${this.titulo}</h3>
${this.renderInsigniaEstado()}
${this.renderSelectorEstado()}
<p>Prioridad: ${this.prioridad}</p>
${this.urgente && html`<p class="aviso">⚠ Urgente</p>`}
${this.cercaDeVencer ? html`<p class="aviso">⏰ Está a punto de vencer</p>` : ''}
${this.expandida
? html`<div class="detalle"><p>Estado interno: la tarjeta está expandida.</p></div>`
: ''}
</article>
`;
}
}Diversos detalls mereixen comentari. Primer, cercaDeVencer es declara com a estat intern ({ state: true }), no com a propietat pública: és una dada calculada dins del propi component a partir de fechaLimite i del rellotge del sistema, no una cosa que cap component extern hagi d'assignar directament, exactament el mateix criteri de expandida vist al mòdul 3. Segon, _calcularSiCercaDeVencer és un mètode auxiliar corrent, sense res especial de Lit, que aïlla la lògica de càlcul de la resta del codi i facilita reutilitzar-la tant a connectedCallback (per a la comprovació inicial) com dins del propi interval. Tercer, i més important per a aquesta lliçó: setInterval s'arrenca a connectedCallback, mai al constructor, precisament perquè no tindria sentit que una <task-card> creada però mai inserida en cap pàgina mantingués un temporitzador corrent indefinidament sense cap efecte visible; i s'atura amb clearInterval a disconnectedCallback, perquè, si la targeta s'elimina de TaskFlow (per exemple, en un futur exercici d'eliminar tasques), el temporitzador deixi d'executar-se i no quedin referències penjades a la memòria.
El resultat, a la pràctica de TaskFlow, és que qualsevol targeta amb una fechaLimite fixada per a dins de menys de 24 hores mostrarà automàticament l'avís "⏰ Está a punto de vencer", sense que ningú hagi de refrescar la pàgina ni canviar cap propietat manualment: el propi pas del temps, vigilat per l'interval, fa que cercaDeVencer passi de false a true en el moment adequat.
- Tancament: què queda per explicar
Amb connectedCallback i disconnectedCallback ja dominats, <task-card> té el seu primer efecte real lligat al pas del temps, correctament iniciat i correctament netejat. Però aquests dos callbacks, heretats de l'estàndard de Custom Elements, no són els únics punts d'enganxament que ofereix Lit sobre el cicle d'una actualització: queden per veure els hooks que Lit afegeix específicament sobre el seu propi procés de renderitzat, capaços de respondre no a "l'element ha entrat o sortit del DOM", sinó a "una propietat reactiva ha canviat i es tornarà a executar render()". Aquest és el contingut de la lliçó següent.
Errors Comuns i Consells
- Oblidar
super.connectedCallback()osuper.disconnectedCallback(): sense aquesta crida, es perd la feina interna queLitElementfa en aquests mateixos callbacks (entre altres coses, la programació de la primera actualització), la qual cosa pot produir errors subtils i difícils de rastrejar, com un component que mai arriba a renderitzar-se la primera vegada. - Arrencar un temporitzador, una subscripció o un listener extern al
constructor: com s'ha explicat a l'apartat 4, elconstructorpot executar-se per a elements que mai arriben a inserir-se al DOM, i no existeix cap callback simètric alconstructorper netejar el que allà s'activi; el lloc correcte és sempreconnectedCallback, amb la seva neteja corresponent adisconnectedCallback. - Oblidar
disconnectedCallbackcompletament: és l'error més habitual i el més costós a llarg termini; qualsevolsetInterval,setTimeoutrecurrent, oaddEventListenerafegit sobrewindowodocument(que no es neteja sol quan el component desapareix, a diferència dels listeners posats sobre el propi Shadow DOM del component) s'ha de desactivar explícitament, o l'aplicació acumularà fuites de memòria cada vegada que es creïn i destrueixin components. - Assumir que
connectedCallbacks'executa una sola vegada en la vida del component: com s'ha explicat a l'apartat 2, un element es pot desconnectar i reconnectar diverses vegades (per exemple, en moure'l d'un contenidor a un altre); unconnectedCallbackmal escrit, que no comprova si ja existeix un interval previ abans de crear-ne un de nou, podria acabar arrencant temporitzadors duplicats si no s'aparella correctament amb el seudisconnectedCallback.
Exercicis
- Modifica
_calcularSiCercaDeVencer()perquè, en lloc d'un únic llindar de "menys de 24 hores", distingeixi tres nivells:lejos(més de 3 dies),proxima(entre 3 dies i 24 hores) iinminente(menys de 24 hores), guardant el resultat en una propietat d'estaturgenciaPorFechade tipusStringen lloc d'un booleà, i ajustarender()per mostrar un missatge diferent segons el nivell. - Explica, basant-te en l'apartat 4, per què hauria estat un error inicialitzar
this._idIntervalo(o arrencar directament elsetInterval) dins delconstructorde<task-card>, encara quefechaLimiteja tingués un valor assignat en aquell moment. - Afegeix a
<task-card>unconsole.logdins deconnectedCallbacki un altre dins dedisconnectedCallback, cada un indicant eltitulode la tasca. Comprova al navegador, inserint i eliminant dinàmicament una<task-card>del DOM amb JavaScript (per exemple, des de la consola ambdocument.body.removeChild(...)idocument.body.appendChild(...)sobre la mateixa referència), que ambdós missatges es disparen a cada cicle d'inserció i retirada, no només una vegada.
Solucions
static properties = {
// ...resto de propiedades...
urgenciaPorFecha: { state: true },
};
_calcularUrgenciaPorFecha() {
if (!this.fechaLimite) {
return 'lejos';
}
const unDiaEnMs = 24 * 60 * 60 * 1000;
const msRestantes = this.fechaLimite.getTime() - Date.now();
if (msRestantes <= 0) {
return 'lejos'; // ja ha vençut; no té sentit continuar avisant
}
if (msRestantes <= unDiaEnMs) {
return 'inminente';
}
if (msRestantes <= 3 * unDiaEnMs) {
return 'proxima';
}
return 'lejos';
}
connectedCallback() {
super.connectedCallback();
this.urgenciaPorFecha = this._calcularUrgenciaPorFecha();
this._idIntervalo = setInterval(() => {
this.urgenciaPorFecha = this._calcularUrgenciaPorFecha();
}, 60000);
}A render(), un simple if/else encadenat (o un petit objecte de missatges indexat pel valor de urgenciaPorFecha) substituiria l'avís únic de cercaDeVencer.
-
Encara que
fechaLimiteja tingués un valor vàlid en el moment d'executar-se elconstructor, el problema no és en el valor de la propietat, sinó en si l'element arribarà a mostrar-se algun cop. Undocument.createElement('task-card')seguit d'una assignació de propietats però sense capappendChildposterior crearia una instància amb unsetIntervalcorrent indefinidament en segon pla, sense capdisconnectedCallbackque el pogués aturar mai (perquè aquest callback només es dispara si l'element va arribar a connectar-se abans).connectedCallbackgaranteix que el temporitzador només existeix mentre l'element està realment en un document visible, i que té sempre undisconnectedCallbackcorresponent capaç de netejar-lo. -
El resultat esperat és que cada crida a
appendChildsobre la referència guardada dispari un nouconnectedCallback(amb el seu corresponent missatge de consola), i cadaremoveChilddispari un noudisconnectedCallback, tantes vegades com es repeteixi l'operació. Això confirma de manera pràctica el que s'ha assenyalat a l'apartat 2: aquests callbacks no són esdeveniments d'"una sola vegada en la vida de l'objecte", sinó que es disparen a cada transició de connexió i desconnexió del DOM, per això la lògica d'arrencada i aturada de l'interval ha de viure exactament en aquesta parella de callbacks i no alconstructor.
Conclusió
Aquesta lliçó ha explicat en detall tres peces del cicle de vida de qualsevol element personalitzat —constructor, connectedCallback i disconnectedCallback— heretades directament de l'estàndard de Custom Elements, sense res específic de Lit pel mig. S'ha fixat la regla pràctica que el constructor inicialitza valors simples, mentre que qualsevol efecte actiu (temporitzadors, subscripcions, listeners externs) pertany a la parella connectedCallback/disconnectedCallback, amb una neteja simètrica i obligatòria per evitar fuites de memòria. <task-card> ja usa aquest patró per avisar automàticament quan una tasca està a prop de la seva fechaLimite, sense necessitat que cap altra propietat canviï per disparar l'avís.
Queden, però, els hooks que Lit afegeix sobre el seu propi procés d'actualització, capaços de reaccionar específicament al fet que una propietat reactiva hagi canviat: willUpdate, firstUpdated, updated i la promesa updateComplete. Aquesta és la peça següent del mòdul 6, i amb ella es completarà finalment l'explicació de quan, exactament, passa una actualització de Lit i en quin ordre s'executen les seves diferents fases.
Curs de Lit
Mòdul 1: Introducció a Lit i Web Components
- Què són els Web Components i per què Lit?
- Configuració de l'Entorn de Desenvolupament
- El Teu Primer Component Lit
- Anatomia d'un Component Lit
Mòdul 2: Plantilles Reactives i Renderitzat
- El Motor de Plantilles de Lit
- Expressions i Interpolació en Plantilles
- Renderitzat Condicional
- Renderitzat de Llistes
- El Cicle de Renderitzat
Mòdul 3: Propietats i Estat Reactiu
- Propietats Reactives
- Estat Intern amb @state
- Tipus de Propietats i Conversors Personalitzats
- Atributs vs Propietats i Reflexió
Mòdul 4: Estils en Components Lit
- CSS Encapsulat amb Shadow DOM
- Estils Compartits entre Components
- Variables CSS Personalitzades i Theming
- Slots i Estilitzat de Contingut Distribuït
Mòdul 5: Esdeveniments i Comunicació entre Components
- Gestió d'Esdeveniments DOM en Plantilles
- Esdeveniments Personalitzats: Comunicació de Fill a Pare
- Comunicació de Pare a Fill amb Propietats
- Patrons de Comunicació entre Components Germans
Mòdul 6: Cicle de Vida i Comportament Avançat
- Callbacks del Cicle de Vida
- Hooks Reactius: willUpdate, updated i firstUpdated
- Controladors Reactius
- Mixins i Composició de Comportament
Mòdul 7: Directives i Funcionalitats Avançades de Plantilles
- Directives Incorporades: classMap, styleMap i ifDefined
- Directives Personalitzades
- Renderitzat Asíncron amb until
- Context Compartit amb @lit/context
Mòdul 8: Integració, Interoperabilitat i Desplegament
- Utilitzar Components Lit en HTML Pla
- Integrar Lit amb React, Vue i Angular
- Renderitzat al Servidor amb @lit-labs/ssr
- Empaquetatge, Publicació i TypeScript
Mòdul 9: Proves i Bones Pràctiques
- Proves Unitàries amb Web Test Runner
- Accessibilitat en Web Components
- Rendiment i Optimització
- Patrons i Antipatrons Comuns
