La lliçó anterior ha resolt el cicle de vida "extern" d'un element personalitzat: quan entra i quan surt del DOM. Però Lit afegeix, per damunt d'aquest cicle heretat de l'estàndard, un segon cicle propi, molt més freqüent durant la vida normal d'un component: el que es dispara cada vegada que una propietat reactiva canvia i Lit decideix tornar a executar render(). El mòdul 2 ja va explicar que aquest procés és asíncron i s'agrupa en microtasques, i la lliçó 03-01 va esmentar de passada updateComplete; aquesta lliçó completa el mapa, ordenant amb precisió les fases d'una actualització i els punts exactes en els quals un component es pot enganxar a cada una d'elles.
Contingut
- El cicle d'actualització complet, pas a pas
shouldUpdate: el veto, esmentat per completesawillUpdate: derivar estat abans de renderitzarrender(): el punt ja conegutfirstUpdated: una sola vegada, després del primer renderupdated: després de cada render, amb el DOM ja pintatupdateComplete: la promesa que tanca el cicle- Taula comparativa dels cinc punts d'enganxament
- Aplicant
willUpdatea<task-card>: urgència derivada de la data - Tancament: cap a la reutilització amb controladors
- El cicle d'actualització complet, pas a pas
Quan una propietat reactiva canvia (o es crida this.requestUpdate() manualment, com es va veure al mòdul 2), Lit no executa simplement render() i ja està: recorre una seqüència fixa de passos, sempre en el mateix ordre, cada un dels quals es pot sobreescriure com a mètode de la classe:
canvi de propietat
│
▼
shouldUpdate(changedProperties) ← pot retornar false i aturar aquí tot el cicle
│
▼
willUpdate(changedProperties) ← abans de renderitzar; el DOM encara no reflecteix el canvi
│
▼
render() ← retorna la plantilla; ja conegut des del mòdul 2
│
▼
(Lit aplica el resultat de render() al DOM real)
│
▼
firstUpdated(changedProperties) ← només la primera vegada, amb el DOM ja actualitzat
│
▼
updated(changedProperties) ← totes les vegades, amb el DOM ja actualitzat
│
▼
this.updateComplete es resol ← promesa observable des de fora del componentTots aquests mètodes, excepte render(), reben un mateix argument: changedProperties, un objecte de tipus Map les claus del qual són els noms de les propietats que han canviat en aquesta actualització concreta, i els valors del qual són el valor anterior de cada una (no el nou: el nou ja està disponible directament a this.nombreDeLaPropiedad). Aquest mapa és l'eina que permet, dins de qualsevol d'aquests hooks, distingir què ha canviat exactament i reaccionar només a allò que interessa, en lloc de recalcular-ho tot en cada actualització sense necessitat.
shouldUpdate: el veto, esmentat per completesa
shouldUpdate: el veto, esmentat per completesaEl primer punt d'enganxament del cicle, shouldUpdate(changedProperties), decideix si l'actualització ha de continuar en absolut. Si es sobreescriu i retorna false, Lit atura el cicle allà mateix: ni willUpdate, ni render(), ni cap altre hook posterior s'executen per a aquesta actualització concreta.
shouldUpdate(changedProperties) {
// Exemple il·lustratiu: ignorar qualsevol actualització mentre
// la targeta estigui en un estat de "només lectura" temporal.
if (this._bloqueadaTemporalmente) {
return false;
}
return true; // el valor per defecte, heretat de LitElement, sempre és true
}És una eina d'optimització de rendiment per a casos concrets (evitar renderitzats costosos quan se sap, d'antuvi, que el resultat visual no canviaria), i no és el focus d'aquesta lliçó: <task-card> i la resta de components de TaskFlow no la necessiten per ara. Es menciona aquí únicament per completar el mapa de l'apartat 1 i perquè quedi clar en quin punt exacte del cicle se situa, abans de passar als hooks que sí que es faran servir activament.
willUpdate: derivar estat abans de renderitzar
willUpdate: derivar estat abans de renderitzarwillUpdate(changedProperties) s'executa just abans de render(), en cada actualització (inclosa la primera). És el lloc pensat específicament per derivar o recalcular estat intern a partir dels canvis de propietats que s'acaben de produir, de manera que aquest càlcul ja estigui disponible quan render() s'executi a continuació, sense haver de repetir la mateixa lògica dins del propi render().
willUpdate(changedProperties) {
if (changedProperties.has('prioridad')) {
this._etiquetaPrioridad = this.prioridad >= 4 ? 'Alta' : 'Normal';
}
}Un detall important: dins de willUpdate és segur assignar noves propietats reactives o camps d'instància normals, perquè encara no s'ha cridat render() en aquesta passada; qualsevol assignació feta aquí queda incorporada a la mateixa actualització en curs, sense disparar un segon cicle complet addicional. Això és just el contrari del que s'advertia al mòdul 2 sobre modificar propietats dins del propi render() (que sí que pot generar bucles d'actualització): willUpdate existeix precisament per donar un lloc segur a aquest tipus de càlcul derivat, abans que la plantilla es construeixi.
changedProperties.has('nombre') és el patró habitual per no recalcular més del necessari: comprovar primer si la propietat rellevant ha canviat en aquesta actualització concreta, i només aleshores executar el càlcul derivat corresponent. Sense aquesta comprovació, willUpdate recalcularia el mateix a cada actualització, fins i tot en aquelles que no tenen res a veure amb aquesta dada en particular; per a càlculs barats no suposa cap problema real, però és un bon costum en quant el càlcul comença a tenir algun cost.
render(): el punt ja conegut
render(): el punt ja conegutrender() ocupa, en aquest cicle més complet, exactament el lloc que ja es coneix des del mòdul 2: construeix i retorna la plantilla html que descriu l'estat actual del component. No hi ha res de nou a afegir aquí excepte la seva posició relativa: s'executa sempre després de willUpdate (que ja ha tingut ocasió de preparar qualsevol dada derivada) i sempre abans que el DOM real quedi actualitzat.
firstUpdated: una sola vegada, després del primer render
firstUpdated: una sola vegada, després del primer renderfirstUpdated(changedProperties) s'executa una única vegada en tota la vida del component: just després que la primera actualització s'hagi aplicat ja al DOM real. A partir d'aquest punt del cicle, a diferència de willUpdate, sí que es pot consultar amb garanties el DOM del propi Shadow Root, perquè ja reflecteix el resultat de render().
firstUpdated(changedProperties) {
// El shadow root ja conté l'<article> retornat pel primer render().
const articulo = this.shadowRoot.querySelector('article');
console.log('Altura inicial de la tarjeta:', articulo.offsetHeight);
}És el lloc recomanat per la pròpia documentació de Lit per a tasques que només té sentit fer una vegada, i que necessiten el DOM ja construït: mesurar la mida real d'un element, posar el focus inicial en un camp, o inicialitzar una llibreria externa de tercers que necessiti un node del DOM real al qual enllaçar-se. Res d'això encaixa a willUpdate (on el DOM encara no s'ha actualitzat) ni tindria sentit repetir-ho en cada actualització posterior, que és just el que distingeix firstUpdated d'updated.
updated: després de cada render, amb el DOM ja pintat
updated: després de cada render, amb el DOM ja pintatupdated(changedProperties) és el germà de firstUpdated que sí que s'executa en totes les actualitzacions, inclosa la primera (de fet, en la primera actualització, Lit crida primer firstUpdated i just després updated; en les següents, només updated). Igual que firstUpdated, s'executa amb el DOM ja actualitzat, així que és segur consultar el Shadow Root amb la certesa que reflecteix el resultat més recent de render().
updated(changedProperties) {
if (changedProperties.has('estado') && this.estado === 'hecha') {
console.log(`La tarea "${this.titulo}" se ha marcado como hecha`);
}
}L'ús típic d'updated és reaccionar a un canvi que ja s'ha pintat a la pantalla: reproduir una animació puntual, sincronitzar alguna cosa amb una llibreria externa que necessita saber que el contingut ha canviat, o (com a l'exemple de l'apartat 9) despatxar un esdeveniment cap a fora únicament quan certa condició passa a complir-se. La comprovació amb changedProperties.has(...) torna a ser fonamental aquí: sense ella, la lògica dins d'updated s'executaria a cada actualització del component, sense importar què hagi canviat realment, la qual cosa sol produir efectes repetits de manera innecessària (o, en el pitjor dels casos, errònia).
updateComplete: la promesa que tanca el cicle
updateComplete: la promesa que tanca el ciclethis.updateComplete, ja introduïda de passada al mòdul 2, és una propietat especial de tipus Promise que es resol exactament quan el cicle complet de l'actualització en curs acaba, és a dir, després que updated s'hagi executat. A diferència dels quatre hooks anteriors, que són mètodes que se sobreescriuen dins de la classe del component, updateComplete està pensada per consultar-se des de fora, per qualsevol codi que necessiti saber quan un component ha acabat d'aplicar els seus canvis més recents:
const tarjeta = document.querySelector('task-card');
tarjeta.estado = 'hecha';
await tarjeta.updateComplete;
// En aquest punt, render(), firstUpdated (si calia) i updated
// ja s'han executat, i el DOM de la targeta reflecteix el nou estat.Dins de la pròpia classe del component, gairebé mai cal usar this.updateComplete: els hooks interns (willUpdate, updated, firstUpdated) ja cobreixen qualsevol necessitat de reaccionar al cicle des de dins. updateComplete resulta més útil en tests automatitzats (esperar que un canvi s'hagi aplicat abans de fer una asserció, cosa que es retindrà al mòdul 9) o en codi extern al propi arbre de components de Lit que necessiti sincronitzar-se amb el ritme d'actualització d'un component concret.
- Taula comparativa dels cinc punts d'enganxament
| Hook | Quan s'executa? | El DOM ja està actualitzat? | Quantes vegades? | Ús principal |
|---|---|---|---|---|
shouldUpdate |
Abans de willUpdate |
No | A cada actualització | Vetar una actualització completa (return false) |
willUpdate |
Just abans de render() |
No | A cada actualització, inclosa la primera | Derivar/recalcular estat intern a partir de propietats canviades |
render() |
Construeix la plantilla | No aplica | A cada actualització | Retornar l'html que descriu l'estat actual |
firstUpdated |
Després d'aplicar el primer render() al DOM |
Sí | Una sola vegada | Mesurar el DOM, enfocar un camp, inicialitzar llibreries externes |
updated |
Després d'aplicar cada render() al DOM |
Sí | A cada actualització, inclosa la primera | Reaccionar a canvis ja pintats a la pantalla |
updateComplete |
Es resol en acabar el cicle | Sí | Una promesa per cada cicle, consultable des de fora | Esperar, des de fora del component, que acabi una actualització |
Una forma senzilla de recordar quan usar willUpdate en front d'updated, que resumeix bé el criteri de tota aquesta lliçó: si la lògica necessita el valor d'una propietat per calcular un altre valor derivat que render() farà servir, va a willUpdate; si la lògica necessita el DOM ja renderitzat, o produeix un efecte cap fora del propi procés de renderitzat, va a updated (o a firstUpdated, si només ha de passar una vegada).
- Aplicant
willUpdate a <task-card>: urgència derivada de la data
willUpdate a <task-card>: urgència derivada de la dataLa lliçó anterior ha afegit a <task-card> un avís de "a prop de vèncer", recalculat periòdicament amb un temporitzador, perquè el pas del temps per si sol pot activar-lo sense que canviï cap propietat. Existeix, però, un problema relacionat però diferent: quan s'assigna una nova fechaLimite a una targeta (per exemple, si en el futur TaskFlow permet editar la data límit d'una tasca ja creada), interessa recalcular immediatament si aquesta nova data cau dins d'un marge "urgent" a curt termini, sense esperar al següent tick del temporitzador i sense duplicar el càlcul directament dins de render(). Aquest és exactament el cas d'ús que willUpdate resol millor que cap altre hook: reaccionar a un canvi de propietat concret, no al pas del temps.
class TaskCard extends LitElement {
static properties = {
// ...resta de propietats sense canvis...
fechaLimite: { converter: conversorDeFecha, attribute: 'fecha-limite' },
cercaDeVencer: { state: true },
};
willUpdate(changedProperties) {
if (changedProperties.has('fechaLimite')) {
// Recalcula immediatament, sense esperar al següent tick del
// temporitzador de la lliçó anterior, en quant la data límit
// canvia per assignació directa de la propietat.
this.cercaDeVencer = this._calcularSiCercaDeVencer();
}
}
// _calcularSiCercaDeVencer(), connectedCallback() i disconnectedCallback()
// continuen exactament igual que a la lliçó anterior.
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>
`;
}
}Val la pena notar com willUpdate i el temporitzador de connectedCallback convivencen sense trepitjar-se: tots dos escriuen a la mateixa propietat d'estat, cercaDeVencer, però reaccionen a disparadors diferents i complementaris. El temporitzador cobreix el cas en el qual res canvia a la targeta, però el rellotge avança (una tasca pot tornar-se urgent sense que ningú la toqui); willUpdate cobreix el cas en el qual la data límit canvia explícitament, i convé reflectir-ho immediatament en la mateixa actualització que ja es va a produir per aquest canvi de propietat, en lloc d'esperar fins al següent tick de l'interval (que podria trigar fins a un minut a disparar-se, segons la configuració de la lliçó anterior). Cap dels dos mecanismes substitueix l'altre: es complementen, cada un cobrint un disparador diferent per a la mateixa dada derivada.
Val la pena remarcar, per tancar aquest apartat, per què aquesta lògica no viu directament dins de render(): si render() cridés this._calcularSiCercaDeVencer() a cada execució, el resultat seria funcionalment equivalent, però s'estaria recalculant aquest valor a cada renderitzat (inclosos els que no tenen res a veure amb fechaLimite, com un simple canvi de prioridad), i a més es perdria la possibilitat de comparar explícitament amb changedProperties.has('fechaLimite') per saber si val la pena recalcular alguna cosa en absolut. willUpdate permet concentrar aquesta decisió en un únic lloc, executada només quan correspon, deixant render() amb la seva responsabilitat original: llegir this.cercaDeVencer ja calculat i traduir-lo a HTML, sense decidir res pel seu compte.
Errors Comuns i Consells
- Confondre
willUpdateambupdated: són oposats quant a l'estat del DOM: dins dewillUpdateel DOM encara no reflecteix els canvis d'aquesta actualització (per això és segur derivar estat allà, però inútil intentar llegir el DOM ja actualitzat); dins d'updated, el DOM ja està al dia (per això és el lloc correcte per llegir-lo o per produir efectes cap a fora, però ja és tard perquè qualsevol canvi de propietat fet allà s'incorpori a aquesta mateixa passada derender()sense provocar una segona actualització). - Oblidar
changedProperties.has(...)dins dewillUpdateoupdated: sense aquesta comprovació, la lògica s'executa a totes les actualitzacions, sense distingir quines són realment rellevants; per a càlculs amb algun cost, o per a efectes que no s'haurien de repetir sense motiu (com eldispatchEventde l'apartat 9), aquesta omissió produeix feina innecessària o comportaments duplicats. - Intentar manipular el DOM del Shadow Root dins de
willUpdate: en aquest punt del cicle, com s'ha explicat, el DOM encara correspon a l'actualització anterior; qualsevolquerySelectorexecutat allà pot retornar un node que desapareixerà o canviarà en acabarrender(). Aquest tipus d'accés al DOM pertany afirstUpdatedoupdated. - Usar
firstUpdatedper a alguna cosa que hauria de repetir-se a cada actualització: si la lògica depèn de dades que canvien amb el temps (comtitulooestado),firstUpdatednomés s'executaria una vegada, amb els valors inicials, i quedaria desactualitzada per sempre; el hook correcte en aquest cas ésupdated.
Exercicis
- Afegeix a
<task-card>un hookupdated(changedProperties)que, únicament la primera vegada quecercaDeVencerpassi defalseatrue(i no a cada actualització posterior mentre continuï senttrue), despatxi un esdeveniment personalitzattarea-proxima-a-vencerambbubbles: trueicomposed: true. Pista:changedProperties.get('cercaDeVencer')conté el valor anterior de la propietat, just el que cal per distingir aquesta transició concreta. - Explica, basant-te en l'apartat 3, per què seria un error escriure la lògica de l'exercici anterior dins de
willUpdateen lloc de dins d'updated. - Un company d'equip proposa eliminar
willUpdatede<task-card>i, en el seu lloc, cridar directamentthis._calcularSiCercaDeVencer()des de dins derender()cada vegada que es necessiti el valor. Explica, recolzant-te en el tancament de l'apartat 9, almenys un desavantatge concret d'aquest enfocament en front de l'ús dewillUpdate.
Solucions
updated(changedProperties) {
if (changedProperties.has('cercaDeVencer')) {
const eraCercaDeVencer = changedProperties.get('cercaDeVencer');
if (!eraCercaDeVencer && this.cercaDeVencer) {
this.dispatchEvent(
new CustomEvent('tarea-proxima-a-vencer', {
detail: { titulo: this.titulo },
bubbles: true,
composed: true,
})
);
}
}
}changedProperties.get('cercaDeVencer') retorna el valor que tenia la propietat abans d'aquesta actualització (false, si és la transició que interessa detectar); comparant aquest valor anterior amb this.cercaDeVencer (ja actualitzat a true) s'aïlla exactament l'instant en què la targeta entra a la finestra d'urgència, sense repetir l'avís en actualitzacions posteriors on cercaDeVencer continuï sent true sense haver canviat.
-
willUpdates'executa abans que el DOM reflecteixi l'actualització en curs, i abans fins i tot querender()s'hagi executat. Despatxar allà un esdeveniment cap a fora seria prematur en el sentit conceptual del cicle: el propi principi d'updated, assenyalat a l'apartat 6, és precisament servir de lloc per a efectes que han de passar després que el canvi ja s'hagi aplicat visualment; un esdeveniment comtarea-proxima-a-vencer, pensat perquè altres components reaccionin a un fet ja consumat, encaixa amb aquest principi, no amb el dewillUpdate, reservat a preparar dades derivades que el propirender()encara farà servir. -
Cridar
this._calcularSiCercaDeVencer()directament dins derender()recalcularia aquest valor a cada execució derender(), incloses les que no tenen absolutament res a veure ambfechaLimite(per exemple, un canvi deprioridado d'expandida), malbaratant feina en un càlcul que, la majoria de les vegades, donaria exactament el mateix resultat que ja tenia. A més, es perdria la possibilitat de reaccionar de forma selectiva només quanfechaLimitecanvia de veritat (mitjançantchangedProperties.has('fechaLimite')), que és exactament l'avantatge que aportawillUpdateen front de repetir el càlcul sense condició dins de la pròpia plantilla.
Conclusió
Aquesta lliçó ha completat el mapa del cicle d'actualització propi de Lit: shouldUpdate com a veto opcional, willUpdate per derivar estat abans de renderitzar, render() en el punt ja conegut, firstUpdated per allò que només ha de passar una vegada amb el DOM ja construït, updated per reaccionar a cada actualització ja pintada, i updateComplete com la promesa que permet a codi extern esperar que aquest cicle acabi. <task-card> ja usa willUpdate per recalcular la seva urgència per data en quant fechaLimite canvia, sense duplicar aquesta lògica dins de render(), complementant el temporitzador de la lliçó anterior en lloc de substituir-lo.
Tota aquesta lògica de temporitzador, però, viu encara directament dins de la classe TaskCard, mesclada amb la resta del seu comportament. Si TaskFlow necessités, més endavant, el mateix tipus d'avís per proximitat de data en un altre component diferent (per exemple, un futur resum de tasques urgents en el propi <task-board>), hauria de duplicar connectedCallback, disconnectedCallback i _calcularSiCercaDeVencer() per complet. La lliçó següent presenta l'eina que Lit ofereix precisament per evitar aquesta duplicació: els controladors reactius.
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
