Introducción al ciclo de vida de los componentes LitElement

  • Por
Qué es el ciclo de vida, cuáles son los métodos del ciclo de vida nativos en Web Components y las particularidades de los updates asíncronos de los templates de LitElement.

El ciclo de vida describe los momentos por los que un componente va pasando a lo largo de su existencia. Para cada uno de los intervalos definidos por el ciclo de vida podemos enganchar código Javascript, que permite realizar acciones clave para el buen funcionamiento del componente.

En LitElement existen una serie de métodos de ciclo de vida, a los que tenemos que unir los métodos que están definidos por el propio estándar Javascript Web Components. En algunos casos son como los "hooks", métodos que nos permiten enganchar determinada funcionalidad en un instante/s dado, pero dentro de los métodos relacionados con el ciclo de vida también encontramos algunos que sirven en realidad para provocar ciertos comportamientos o pasar al componente de un estado a otro.

En este artículo me dedicaré a aclarar las claves para dominar el ciclo de vida de componentes en LitElement, sin detallar todavía cada uno de sus métodos.

Métodos nativos de la especificación Web Components

Antes de conocer las particularidades del ciclo de vida de LitElement, debemos tener claros los métodos nativos del ciclo de vida, que siempre están ahí para ayudarnos si los necesitamos. Estos son propios de Javascript y por tanto están disponibles en cualquier Web Component, hecho o no con LitElement:

  • Método constructor(): es el constructor de la clase, que sabemos que resume las tareas de inicialización de los objetos. Este constructor se usa generalmente para inicializar propiedades, pero debemos tener en cuenta que cuando se ejecuta el constructor no están disponibles algunas cosas del componente, como por ejemplo el shadow DOM o los elementos del template.
  • Método connectedCallback(): Se ejecuta cuando el componente se inyecta en el DOM de la página o en el Shadow DOM de un componente. Este método es especialmente útil para realizar acciones cuando un componente está realmente en la página, porque podría ocurrir que el componente se haya creado solamente en la memoria de Javascript, pero que no se haya incorporado en ningún template ni en el propio documento HTML.
  • Método disconnectedCallback(): Se ejecuta cuando un componente es retirado del DOM. Muchas veces es necesario usarlo cuando un componente ha definido un comportamiento en el connectedCallback(), que muy probablemente tendremos que anular si el componente se retira de la página, por medio de disconnectedCallback.
  • Método attributeChangedCallback(): este método se ejecuta cada vez que un atributo de la página altera su valor. Pero ojo, estamos hablando de atributos y no propiedades. Para que se ejecute este método debería cambiarse el atrubuto en la etiqueta del componente (etiqueta host).
  • Método adoptedCallback(): Se ejecuta al producirse un cambio de un elemento, que se traslada hacia otro nuevo documento. (Pertenece al estándar pero, sinceramente, nunca lo he usado). Además debes saber que este método del ciclo de vida no está incluido dentro de las características que están emuladas por el polyfill, por lo que si no el navegador no tiene soporte nativo, no entrará en juego.

Ejemplos de métodos nativos del ciclo de vida

Lo más importante al usar los métodos nativos del ciclo de vida es acordarnos siempre de invocar al mismo método del ciclo de vida, pero de la clase padre. Pues si no lo hacemos no se desencadenarán los comportamientos propios de LitElement y las cosas empezarán a fallar de manera imprevisible.

Por ejemplo, cuando usamos el constructor para inicializar una propiedad, es importante que llamemos al constructor de la clase padre, con la invocación super().

constructor() {
  super();
  this.prop1 = 'Inicialización de una propiedad!'
}

El método attributeChangedCallback() es el más complicado de usar, ya que recibe varios parámetros, que nos informan sobre qué atributos acaban de cambiar y cuáles son los valores del atributo, antiguo y nuevo.

attributeChangedCallback(att, oldvalue, newvalue) {
  super.attributeChangedCallback(att, oldvalue, newvalue);
  console.log('Se ha producido attributeChangedCallback por el atributo', att);
}
Nota: es importante fijarse en la llamada al método attributeChangedCallback() de la clase padre, porque si no apreciaremos que el template no se llega a actualizar nunca, a pesar que las propiedades del componente cambien de valor. Lo hacemos con super.attributeChangedCallback() y por supuesto, tenemos que enviar todos los parámetros recibidos en el método originalmente.

Como hemos señalado, este método se ejecutará cuando cambia el atributo de la etiqueta del componente.

<mi-componente prop1="Valor del atributo"></mi-componente>

Al usar este componente, se invocaría inicialmente el método attributeChangedCallback(), dado que se ha seteado el atributo en la etiqueta, lo que implica una modificación desde el valor inicial, que sería siempre undefined.

Pero sin embargo, si cambiamos la propiedad dentro del componente:

onClick() {
  this.prop1 = Math.floor(Math.random() * 100);
}

Podremos observar que el método attributeChangedCallback no tiene por qué ejecutarse de nuevo, porque en este caso solo se ha cambiado la propiedad interna del componente, lo que no repercute necesariamente en un cambio en el atributo.

Nota: para producir un cambio en el atributo con cada cambio de la propiedad, necesitamos declarar la propiedad con "reflect" a true. La configuración de reflect la puedes definir al declarar las propiedades, en el método "static get properties()". Lo veremos cuando analicemos más de cerca las propiedades de LitElement.

Updates asíncronos del template

Lo más importante que debemos tener en cuenta para entender el ciclo de vida en LitElement es que los updates en el template son asíncronos. Como sabemos, una de las ventajas de trabajar con LitElement es que los templates son reactivos, de modo que, al actualizarse las propiedades del componente, éstas se propagan por el template. Pues bien, debe quedarnos claro que esta propagación se realiza de manera asíncrona.

Esto quiere decir que, al almacenarse un nuevo valor en una propiedad, pueden pasar unos pequeños instantes antes que el template se actualice. Cuestión de milisegundos, imperceptible para un humano, pero a la hora de realizar la programación del componente es importante saberlo, porque ello nos puede llevar a algunas situaciones delicadas.

Nota: El programador de Javascript seguramente tenga claro que implica esta situación, pues entendemos que debe estar acostumbrado a la asincronía del lenguaje. Sabemos que el código en Javascript no tiene por qué ejecutarse de manera puramente secuencial. Existen determinadas funciones que tardan un tiempo en ejecutarse y que durante ese tiempo de espera, Javascript sigue ejecutando instrucciones.

El motivo de esta asincronía es debido puramente a la optimización del rendimiento. En lugar de actualizarse el template por cada propiedad singular alterada, LitElement puede que se puede actualizarlo una sola vez y reflejar el cambio de de varias propiedades al mismo tiempo.

Esquema general de actualización del template

A modo de resumen, este es el ciclo de actualizaciones de un template en LitElement, como consecuencia del cambio de una propiedad:

  1. La propiedad se cambia
  2. Se puede decidir si el cambio anterior tiene, o no, que producir una actualización del template.
  3. En caso que el template se necesite actualizar, simplemente se requiere dicho update.
  4. En el próximo ciclo del "event loop" se realiza la actualización o actualizaciones del template requeridas. Este proceso puede incluir la transformación del valor de propiedades o atributos, junto con el nuevo render del template propiamente dicho.
  5. Por último, LitElement resuelve una promesa, para indicar que el update ha sido completado.

Event loop del navegador

Por si no lo conocemos, sería ideal ofrecer algunas notas breves sobre lo que es el event loop. Básicamente es una de las herramientas que el lenguaje Javascript dispone para gestionar la asincronía y conseguir su característica de lenguaje de programación "no bloqueante".

El event loop es como un bucle infinito, que el navegador ejecuta continuamente para resolver las tareas que se hayan definido en una cola. Cada ciclo del event loop permite ejecutar comportamientos que se hayan colocado en la cola y así atender callbacks que estén pendientes de ejecución después de resolverse comportamientos asíncronos.

En resumen, si tenemos un código secuencial, púramente síncrono, las instrucciones se procesan instantáneamente, una detrás de otra. Pero si el código que se ejecuta tiene un comportamiento asíncrono, la función callback a procesar se va a una cola de callbacks. Solo cuando ha terminado el proceso asíncrono el callback se introduce en la pila de ejecución, para que sea procesada en el siguiente ciclo del event loop.

Este funcionamiento del navegador y su event loop es lo que permite que el la ventana de navegación esté activa, sin bloquear su interfaz, aunque haya procesos pendientes de resolverse, como por ejemplo respuestas Ajax a un servicio web. En el caso concreto de LitElement, se utiliza el event loop para programar cambios en el template, que solo producirán la correspondiente actualización por medio de una micro tarea, en el siguiente ciclo de ejecución de event loop. Además, este mismo mecanismo permite que el procesamiento de rutinas de LitElement sean lo más ligeras posibles, desde el punto de vista de evitar el bloqueo del navegador del usuario.

Cómo ejecutar código cuando el template ha sido actualizado

Dado que un cambio de una propiedad del componente no produce instantáneamente la actualización del template, podemos encontrarnos en una situación en la que estemos obligados a esperar la propagación de dicha propiedad en el sistema de binding, antes de realizar determinadas acciones adicionales. Para estos casos en LitElement tenemos un procedimiento específico, que nos permite enganchar código cuando esa actualización ha sido llevada a cabo.

En concreto disponemos de la propiedad updateComplete, que contiene una promesa que se resolverá después de la correspondiente actualización del template.

Por tanto, podríamos tener algo como esto:

onClick() {
  this.prop1 = 'nuevo valor';
  this.updateComplete.then(() => {
      console.log('El template se ha actualizado ya!!');
    }) 
}

Este método realiza la actualización de una propiedad. Esa actualización se encola para ejecutarse en el próximo ciclo del event loop.

A continuación esperamos a resolverse la promesa "this.updateComplete". Por tanto, en el caso positivo de esa promesa, definida con el método then(), podemos estar seguros que los cambios han sido reflejados en el template.

Conclusión

En este artículo hemos repasado las claves para entender las particularidades del ciclo de vida de LitElement. Teniendo claros los conceptos explicados en este artículo, sobre todo en lo que respecta a los updates asíncronos, estamos en disposición de aprender a manejar bien los aspectos del "livecycle" de LitElement.

Hemos conocido los métodos nativos del ciclo de vida de componentes estándar, pero hay muchos más que son específicos de Litelement que todavía debes de aprender, que serán materia de estudio en sucesivos artículos.

Autor

Miguel Angel Alvarez

Miguel es fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Comenzó en el mundo del desarrollo web en el año 1997, transformando su hobby en su trabajo.

Compartir