MutationObserver
es un objeto incorporado que observa un elemento DOM y dispara un callback cuando hay cambios en él.
Primero veremos su sintaxis, luego exploraremos un caso de la vida real para ver dónde puede ser útil.
Sintaxis
MutationObserver
es fácil de usar.
Primero creamos un observador con una función callback:
let observer = new MutationObserver(callback);
Y luego lo vinculamos a un nodo DOM:
observer.observe(node, config);
config
es un objeto con opciones booleanas “a qué clase de cambios reaccionar”:
childList
– cambios en los hijos directos denode
,subtree
– en todos los descendientes denode
,attributes
– atributos denode
,attributeFilter
– un array de nombres de atributos, para observar solamente a los seleccionados,characterData
– establece si debe observar cambios de texto ennode.data
o no,
Algunas otras opciones:
attributeOldValue
– si estrue
, tanto el valor viejo como el nuevo del atributo son pasados al callback (ver abajo), de otro modo pasa solamente el nuevo (necesita la opciónattributes
),characterDataOldValue
– si estrue
, tanto el valor viejo como el nuevo denode.data
son pasados al callback (ver abajo), de otro modo pasa solamente el nuevo (necesita la opcióncharacterData
).
Entonces, después de cualquier cambio, el callback
es ejecutado: los cambios son pasados en el primer argumento como una lista objetos MutationRecord, y el observador en sí mismo como segundo argumento.
Los objetos MutationRecord tienen como propiedades:
type
– tipo de mutación, uno de:"attributes"
: atributo modificado,"characterData"
: dato modificado, usado para nodos de texto,"childList"
: elementos hijos agregados o quitados,
target
– dónde ocurrió el cambio: un elemento para"attributes"
, o un nodo de texto para"characterData"
, o un elemento para una mutación de"childList"
,addedNodes/removedNodes
– nodos que fueron agregados o quitados,previousSibling/nextSibling
– los nodos “hermanos”, previos y siguientes a los nodos agregados y quitados,attributeName/attributeNamespace
– el nombre o namespace (para XML) del atributo cambiado,oldValue
– el valor previo, solamente cambios de atributo o cambios de texto si se establece la opción correspondienteattributeOldValue
/characterDataOldValue
.
Por ejemplo, aquí hay un <div>
con un atributo contentEditable
. Ese atributo nos permite poner el foco en él y editarlo.
<div contentEditable id="elem">Click and <b>edit</b>, please</div>
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(los cambios)
});
// observa todo exceptuando atributos
observer.observe(elem, {
childList: true, // observa hijos directos
subtree: true, // y descendientes inferiores también
characterDataOldValue: true // pasa el dato viejo al callback
});
</script>
Si ejecutamos este código en el navegador, el foco en el <div>
dado y el cambio en texto dentro de <b>edit</b>
, console.log
mostrará una mutación:
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// otras propiedades vacías
}];
Si hacemos operaciones de edición más complejas, como eliminar el <b>edit</b>
, el evento de mutación puede contener múltiples registros de mutación:
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// otras propiedades vacías
}, {
type: "characterData"
target: <text node>
// ...detalles de mutación dependen de cómo el navegador maneja tal eliminación
// puede unir dos nodos de texto adyacentes "edit " y ", please" en un nodo
// o puede dejarlos como nodos de texto separados
}];
Así, MutationObserver
permite reaccionar a cualquier cambio dentro del subárbol DOM.
Uso para integración
¿Cuándo puede ser práctico esto?
Imagina la situación cuando necesitas añadir un script de terceros que contiene funcionalidad útil, pero también hace algo no deseado, por ejemplo añadir publicidad <div class="ads">Unwanted ads</div>
.
Naturalmente el script de terceras partes no proporciona mecanismos para removerlo.
Usando MutationObserver
podemos detectar cuándo aparece el elemento no deseado en nuestro DOM y removerlo.
Hay otras situaciones, como cuando un script de terceras partes agrega algo en nuestro documento y quisiéramos detectarlo para adaptar nuestra página y cambiar el tamaño de algo dinámicamente, etc.
MutationObserver
permite implementarlo.
Uso para arquitectura
Hay también situaciones donde MutationObserver
es bueno desde el punto de vista de la arquitectura.
Digamos que estamos haciendo un sitio web acerca de programación. Naturalmente, los artículos y otros materiales pueden contener fragmentos de código.
Tal fragmento en un markup HTML se ve como esto:
...
<pre class="language-javascript"><code>
// aquí el código
let hello = "world";
</code></pre>
...
Para mejorar la legibilidad y al mismo tiempo embellecerlo, usaremos una librería JavaScript de “highlighting” para resaltar elementos de nuestro sitio, por ejemplo Prism.js. Para obtener sintaxis resaltada para el fragmento de arriba en Prism, llamamos a Prism.highlightElem(pre)
, que examina el contenido de tales elementos y les agrega tags y styles especiales para obtener sintaxis resaltada con color, similares a los que ves en esta página.
¿Exactamente cuándo ejecutar tal método de highlighting? Bien, podemos hacerlo en el evento DOMContentLoaded
, o poner el script al final de la página. En el momento en que tenemos nuestro DOM listo buscamos los elementos pre[class*="language"]
y llamamos Prism.highlightElem
en ellos:
// resaltar todos los fragmentos de código en la página
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
Todo es simple hasta ahora, ¿verdad? Buscamos fragmentos de código en HTML y los resaltamos.
Continuemos. Digamos que vamos a buscar dinámicamente material desde un servidor. Estudiaremos métodos para ello más adelante en el tutorial. Por ahora solamente importa que buscamos un artículo HTML desde un servidor web y lo mostramos bajo demanda:
let article = /* busca contenido nuevo desde un servidor */
articleElem.innerHTML = article;
El nuevo elemento HTML article
puede contener fragmentos de código. Necesitamos llamar Prism.highlightElem
en ellos, de otro modo no se resaltarían.
¿Dónde y cuándo llamar Prism.highlightElem
en un artículo cargado dinámicamente?
Podríamos agregar el llamado al código que carga un “article”, como esto:
let article = /* busca contenido nuevo desde un servidor */
articleElem.innerHTML = article;
let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);
…Pero imagina que tenemos muchos lugares en el código donde cargamos contenido: artículos, cuestionarios, entradas de foros. ¿Necesitamos poner el llamado al “highlighting” en todos lugares? Eso no es muy conveniente.
¿Y si el contenido es cargado por un módulo de terceras partes? Por ejemplo tenemos un foro, escrito por algún otro, que carga contenido dinámicamente y quisiéramos añadirle sintaxis resaltada. A nadie le gusta emparchar scripts de terceras partes.
Afortunadamente hay otra opción.
Podemos usar MutationObserver
para detectar automáticamente cuándo los fragmentos de código son insertados en la página y resaltarlos.
Entonces manejaremos la funcionalidad de “highlighting” en un único lugar, liberándonos de la necesidad de integrarlo.
Demo de highlight dinámico
Aquí el ejemplo funcionando.
Si ejecutas el código, este comienza a observar el elemento debajo y resalta cualquier fragmento de código que aparezca allí:
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
// examine nodos nuevos, ¿hay algo para resaltar?
for(let node of mutation.addedNodes) {
// seguimos elementos solamente, saltamos los otros nodos (es decir nodos de texto)
if (!(node instanceof HTMLElement)) continue;
// verificamos que el elemento insertado sea un fragmento de código
if (node.matches('pre[class*="language-"]')) {
Prism.highlightElement(node);
}
// ¿o tal vez haya un fragmento de código en su sub-árbol?
for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
Prism.highlightElement(elem);
}
}
}
});
let demoElem = document.getElementById('highlight-demo');
observer.observe(demoElem, {childList: true, subtree: true});
Aquí, abajo, hay un elemento HTML y JavaScript que lo llena dinámicamente usando innerHTML
.
Por favor ejecuta el código anterior (arriba, que observa aquel elemento) y luego el código de abajo. Verás cómo MutationObserver
detecta y resalta el fragmento.
A demo-element with id="highlight-demo"
, run the code above to observe it.
El siguiente código llena su innerHTML
, lo que causa que MutationObserver
reaccione y resalte su contenido:
let demoElem = document.getElementById('highlight-demo');
// inserta contenido con fragmentos de código
demoElem.innerHTML = `A code snippet is below:
<pre class="language-javascript"><code> let hello = "world!"; </code></pre>
<div>Another one:</div>
<div>
<pre class="language-css"><code>.class { margin: 5px; } </code></pre>
</div>
`;
Ahora tenemos un MutationObserver
que puede rastrear todo el “highlighting” en los elementos observados del document
entero. Podemos agregar o quitar fragmentos de código en el HTML sin siquiera pensar en ello.
Métodos adicionales
Hay un método para detener la observación del nodo:
observer.disconnect()
– detiene la observación.
Cuando detenemos la observación, algunos cambios todavía podrían quedar sin ser procesados por el observador. En tales casos usamos
observer.takeRecords()
– obtiene una lista de registros de mutaciones sin procesar, aquellos que ocurrieron pero el callback no manejó.
Estos métodos pueden ser usados juntos, como esto:
// obtener una lista de mutaciones sin procesar
// debe ser llamada antes de la desconexión,
// si te interesa las posibles mutaciones recientes sin manejar
let mutationRecords = observer.takeRecords();
// detener el rastreo de cambios
observer.disconnect();
...
observer.takeRecords()
son quitados de la cola de procesamientoEl callback no será llamado en registros devueltos por observer.takeRecords()
.
Los observadores usan internamente referencias débiles a nodos. Esto es: si un nodo es quitado del DOM y se hace inalcanzable, se vuelve basura para ser recolectada.
El mero hecho de que un nodo DOM sea observado no evita la recolección de basura.
Resumen
MutationObserver
puede reaccionar a cambios en el DOM: atributos, contenido de texto y añadir o quitar elementos.
Podemos usarlo para rastrear cambios introducidos por otras partes de nuestro código o bien para integrarlo con scripts de terceras partes.
MutationObserver
puede rastrear cualquier cambio. Las opciones de config
permiten establecer qué se va a observar, se usa para optimización y no desperdiciar recursos en llamados al callback innecesarios.