MutationObserver: DOM reactivo

Detectar cambios en el DOM de forma reactiva


Trabajar con el DOM mediante Javascript nativo, muchas veces se suele tachar de algo que es demasiado complicado porque tienes que estar actualizando los datos, y por separado la interfaz de usuario. Sin embargo, esto no tiene que ser necesariamente así, sino que se hace muchas veces por desarrollar más rápido o simplemente por desconocimiento, ya que existe una API denominada MutationObserver.

¿Qué es MutationObserver?

MutationObserver es una API del navegador que pertenece a la familia de observadores. Los observadores, como su propio nombre indica, son sistemas que vigilan a otros y realizan una acción cuando detectan cambios u operaciones.

En el caso de MutationObserver, se trata de un observador que vigila un elemento del DOM para detectar cuando se realizan cambios en ese elemento del DOM observado. Si ocurre un cambio, se ejecuta una lógica previamente definida en el observador.

La API MutationObserver

La API MutationObserver tiene varios métodos para utilizar según lo que se desee. En principio, lo que haremos es crear una nueva instancia de MutationObserver, a la cuál se le pasará una función por parámetro con la lógica que queramos ejecutar cuando se detecte un cambio en el lugar del DOM deseado:

const observer = new MutationObserver(() => {
  console.log("Cambio detectado en el contenedor.");
});

Como se puede ver, la instancia MutationObserver observer tiene una funcionalidad (un console.log) que se ejecutará cuando detecte los cambios en el DOM que indiquemos un poco más tarde. Para ello, utilizaremos el método observe() sobre esta instancia recién creada. Además, hay algunos otros métodos disponibles:

MétodosDescripción
observe(target, options)Comienza a observar cambios en el elemento target del DOM.
disconnect()Detiene la observación del objeto.
takeRecords()Elimina todas las notificaciones pendientes y las devuelve en un .

Observa que también utilizamos el método disconnect() para detener las observaciones realizadas o la utilización del método takeRecords(), que además de detener las pendientes, nos las devuelve en un array por si necesitamos trabajar con ellas posteriormente.

Veamos un ejemplo sencillo donde vamos a escuchar los cambios en el DOM de un elemento con clase .container. Cada vez que se hagan cambios en ese elemento del DOM, se disparará la función que le pasamos al MutationObserver al crear nuestra instancia. Dicha función sólo escribe un mensaje en la consola del navegador, por lo que será conveniente abrirla:

<div class="container">No messages.</div>

<script>
const container = document.querySelector(".container");

// Añade aleatoriamente un nuevo mensaje al DOM
const addRandomMessage = () => {
  const time = 1000 + Math.floor(Math.random() * 3000);
  container.insertAdjacentHTML("afterbegin", `<div>¡Mensaje recibido!</div>`);
  setTimeout(() => addRandomMessage(), time);
}

// Dispara una acción cuando ocurre un cambio en el DOM
const observer = new MutationObserver(() => {
  console.log("Cambio detectado en el contenedor.");
});

// Iniciamos observación
observer.observe(container, { childList: true });

// Provocamos cambios en el DOM
setTimeout(() => addRandomMessage(), 3000);
</script>

Por otro lado, observa que la función addRandomMessage() se va a llamar aleatoriamente cada ciertos segundos y añadirá un nuevo mensaje en el contenedor. De esta forma, cada cierto número de segundos, se debería disparar el console.log() con el mensaje "Cambio detectado en el contenedor".

Opciones de observación

En el ejemplo anterior, sólo incluimos la opción childList a true en nuestro objeto de opciones, sin embargo podemos utilizar muchas otras opciones para personalizar la observación:

Opción de observaciónPor defectoDescripción
childListfalseVigila cambios en el elemento target (sin incluir hijos de hijos).
subtreefalseVigila cambios en los hijos de hijos.
attributesfalseDetecta cambios en los atributos del nodo observado.
attributeFilter-Indica que atributos observar. Si se omite, se vigilan todos.
attributeOldValuefalseIndica si se deben guardar los valores anteriores del cambio.
characterDatafalseVigila cambios en los nodos de texto.
characterDataOldValuefalseIndica si se deben guardar los valores anteriores en los nodos de texto.

Especialmente importante la opción subtree, ya que esta opción nos puede permitir indicar vigilar no sólo el elemento del DOM y sus cambios en hijos, sino también de los hijos de los hijos de ese elemento.

<div class="container">
  <div class="message-list">
    No messages.
  </div>
</div>

<script>
const container = document.querySelector(".container");
const messageList = document.querySelector(".message-list");

const addRandomMessage = () => {
  const time = 1000 + Math.floor(Math.random() * 3000);
  messageList.insertAdjacentHTML("afterbegin", `<div>¡Mensaje recibido!</div>`);
  setTimeout(() => addRandomMessage(), time);
}

const observer = new MutationObserver((mutationsList, observer) => {
  console.log("Cambio detectado en el contenedor: ", mutationsList);
});

observer.observe(container, { childList: true, subtree: true });

setTimeout(() => addRandomMessage(), 3000);

// Cuando ya no se necesite el observador
// observer.disconnect();
</script>

Lista de mutaciones

Si observas detenidamente el ejemplo anterior, te darás cuenta que definimos un primer parámetro mutationsList que no llegamos a utilizar y lo mismo con el siguiente parámetro. El observer simplemente es el observador implicado, por si queremos hacer referencia a él.

Sin embargo, el mutationsList es un con las mutaciones detectadas, donde hay varias propiedades que podrían resultar interesantes:

PropiedadDescripciónAplica cuando type es...
typeTipo de mutación detectada: childList, attributes o characterData.
targetNodo del DOM en el que ocurrió el cambio.
addedNodesLista de nodos agregados.childList
removedNodesLista de nodos eliminados.childList
attributeNameNombre del atributo modificado.attributes
oldValueValor anterior del atributo o nodo de texto modificado.attributes o characterData

Observa este fragmento y fíjate como recorremos el mutationsList con un forEach(), ya que podemos estar trabajando con una sola mutación o con varias de ellas. De esta forma, podemos saber si se trata de un cambio de elementos del DOM o de atributos en el elemento:

const observer = new MutationObserver((mutationsList, observer) => {
    mutationsList.forEach(mutation => {
        console.log(`Tipo de cambio: ${mutation.type}`);
        if (mutation.type === 'childList') {
            console.log("Nodos agregados: ", mutation.addedNodes);
            console.log("Nodos eliminados: ", mutation.removedNodes);
        } else if (mutation.type === 'attributes') {
            console.log(`Atributo modificado: ${mutation.attributeName}`);
        }
    });
});

observer.observe(targetNode, {
  attributes: true,
  childList: true,
  subtree: true
});

Como puedes ver, la API de MutationObserver es una fantástica forma de detectar cambios en el DOM de forma automática y hacer un poco más predecibles nuestras aplicaciones web.

¿Quién soy yo?

Soy Manz, vivo en Tenerife (España) y soy streamer partner en Twitch y profesor. Me apasiona el universo de la programación web, el diseño y desarrollo web y la tecnología en general. Aunque soy full-stack, mi pasión es el front-end, la terminal y crear cosas divertidas y locas.

Puedes encontrar más sobre mi en Manz.dev