Il MutationObserver è un oggetto incorporato (built-in object) che osserva un elemento DOM e lancia una callback in caso di variazioni.
Prima di tutto vediamo la sintassi, dopodiché analizzeremo un un caso dâuso del mondo reale.
Sintassi
Il MutationObserver è semplice da usare.
Come prima cosa, creiamo un observer con una funzione di callback:
let observer = new MutationObserver(callback);
quindi lo andiamo ad associare ad un nodo del DOM:
observer.observe(node, config);
config è un oggetto che indica delle informazioni del tipo âa quale tipo di variazioni reagireâ del tipo:
childListâ cambiamenti nei nodi figli (childrens) dinode,subtreeâ in tutti i nodi discendenti dinode,attributesâ attributi dinode,attributeFilterâ un array di attributi, per osservare solo quelli selezionati.characterDataâ se bisogna osservarenode.data(text content),
Poche altre opzioni:
attributeOldValueâ se impostato atrue, passerà alla callback sia il vecchio che il nuovo valore dellâattributo (più informazioni in basso), altrimenti (false) passerà solamente il nuovo valore (necessita dellâopzioneattributes).characterDataOldValueâ se impostato atrue, passerà alla callback sia il vecchio che il nuovo valore dinode.data(più informazioni in basso), altrimenti (false) passerà solamente il nuovo valore (necessita dellâopzionecharacterData).
Quindi, dopo ogni variazione, la callback verrà eseguita: le variazioni vengono passate nel primo argomento come una lista di oggetti MutationRecord, e lâobserver stesso come secondo argomento.
Gli oggetti MutationRecord objects hanno proprietà :
typeâ tipo di mutation, uno tra:"attributes": attributi modificati"characterData": dati modificati, utilizzati per i nodi testuali,"childList": elementi figlio aggiunti/rimossi,
targetâ dove la variazione è avvenuta: un elemento per"attributes", o nodo testuale per"characterData", o un elemento per un"childList"mutation,addedNodes/removedNodesâ nodi che sono stati aggiunti/rimossi,previousSibling/nextSiblingâ il nodo di pari livello precedente e successivo rispetto al nodo aggiunto/rimosso,attributeName/attributeNamespaceâ il nome/spazio dei nomi (name/namespace) (per XML) dellâattributo variato,oldValueâ il valore precedente, solo per variazioni di attributi o di testo, se è settata la relativa opzioneattributeOldValue/characterDataOldValue.
Per esempio, abbiamo un <div> con un attributo contentEditable. Questo attributo ci permette di mettere il focus su di esso per poterlo modificare.
<div contentEditable id="elem">Clicca e <b>modifica</b>, per piacere.</div>
<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(le modifiche)
});
// observe everything except attributes
observer.observe(elem, {
childList: true, // observe direct children
subtree: true, // and lower descendants too
characterDataOldValue: true // pass old data to callback
});
</script>
Adesso, se noi andiamo a modificare il testo allâinterno di <b>modifica</b>, otterremo una sola mutation (single mutation):
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// other properties empty
}];
Se invece selezioniamo e rimuoviamo del tutto <b>modifica</b>, otterremo una più mutation (multiple mutations):
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// altre proprietà vuote
}, {
type: "characterData"
target: <text node>
// ...i dettagli della mutation dipendono da come il browser implementa questa rimozione
// potrebbe fondere i due nodi adiacenti "modifica " e ", per piacere" in un nodo
// oppure potrebbe lasciarli in due nodi testuali separati
}];
Quindi, MutationObserver permette di reagire a qualunque variazione allâinterno degli sottonodi del DOM.
Utilizzo per lâintegrazione
Quando questa cosa può essere utile?
Immagina la situazione in cui devi inserire uno script di terze parti che aggiunge delle funzionalità utili nella pagina, ma anche che faccia qualcosa di non richiesto, ad esempio, che mostri delle pubblicità <div class="ads">Unwanted ads</div>.
Ovviamente, lo script di terze parti non fornisce alcun meccanismo per rimuoverla.
Il MutationObserver può gestirlo facilmente.
Utilizzo per lâarchitettura
Ci sono anche delle situazioni in cui il MutationObserver è di utilità dal punto di vista architetturale.
Diciamo che stiamo creando un sito sulla programmazione. Naturalmente, gli articoli ed altro materiale può contenere dei frammenti di codice (snippets).
Questi frammenti nel markup HTML appaiono in questa maniera:
...
<pre class="language-javascript"><code>
// qui il codice
let hello = "world";
</code></pre>
...
Utilizziamo anche una libreria di evidenziazione del codice Javascript, ad esempio Prism.js. Una chiamata al metodo Prism.highlightElem(pre) esaminerà il contenuto di questo elemento pre e lo inserirà dentro dei tags specifici, generando degli stili CSS appositi per creare la colorazione dellâevidenziazione della sintassi, similmente a quello che puoi notare nellâesempio di questa pagina.
Quando esattamente eseguire questo metodo per lâevidenziazione? Possiamo farlo nellâevento DOMContentLoaded, oppure alla fine della pagina. Nel momento in cui abbiamo il DOM pronto, potrà effettuare la ricerca degli elementi pre[class*="language"] e chiamare il metodo Prism.highlightElem su di essi.
// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
Finora è tutto molto semplice, giusto? Ci sono degli frammenti di codice<pre> nellâHTML, e noi li coloriamo.
Adesso andiamo avanti. Diciamo che andiamo ad eseguire il âfetchâ di materiale da un server. Studieremo questi metodi più avanti nel tutorial. Per adesso importa solamente che noi eseguiamo una richiesta (utilizzando il metodo âfetchâ) di un articolo HTML da un webserver lo lo visualizziamo on demand:
let article = /* fetch new content from server */
articleElem.innerHTML = article;
Il nuovo article HTML potrebbe contenere dei frammenti di codice. Abbiamo bisogno di chiamare Prism.highlightElem su di essi, altrimenti il loro codice non verrà evidenziato.
Dove e quando chiamare Prism.highlightElem per un articolo caricato dinamicamente?
Potremmo accodare questa chiamata al codice che carica un articolo, così:
let article = /* fetch new content from server */
articleElem.innerHTML = article;
let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);
â¦Ma immaginiamo, abbiamo un sacco di punti nel codice nei quali carichiamo contenuti, articoli, quiz, messaggi di forum. Dovremmo mettere la chiamata per lâevidenziazione dovunque? Non sarebbe molto conveniente ed è anche facile dimenticare di farlo ogni volta.
E che succederebbe, inoltre, se il contenuto venisse caricato da un modulo terze parti? Ad esempio, abbiamo un forum scritto da qualcuno altro, che carica contenuti dinamicamente, e ci piacerebbe aggiungergli lâevidenziazione del codice. A nessuno piace rattoppare script di terze parti.
Fortunatamente, esiste unâaltra opzione.
Possiamo usare il MutationObserver per rilevare automaticamente i frammenti di codice che vengono inseriti nella pagina ed evidenziarli.
In questo modo gestiamo la funzionalità di evidenziazione soltanto in un posto, liberandoci della necessità di integrarla.
Demo di evidenziazione dinamica
Qui lâesempio funzionante.
Se esegui questo codice, comincerà osservando lâelemento sotto ed evidenziando qualunque frammento di codice che apparirà lì:
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
// esamina i nuovi nodi, c'e' qualcosa da evidenziare?
for(let node of mutation.addedNodes) {
// teniamo traccia solo degli elementi, ignorando sugli altri nodi (ad esempio i nodi testuali)
if (!(node instanceof HTMLElement)) continue;
// controlla che l'elemento inserito sia un frammento di codice (code snippet)
if (node.matches('pre[class*="language-"]')) {
Prism.highlightElement(node);
}
// o forse c'e' un frammento di codice da qualche parte tra i suo nodi figlio?
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});
Qui câè un elemento HTML e JavaScript che lo riempie dinamicamente utilizzando innerHTML.
Esegui il codice precedente (sopra, osserverà quellâelemento), e poi il codice sotto. Vedrai come il MutationObserver rileverà ed evidenzierà il frammento.
A demo-element with id="highlight-demo", run the code above to observe it.
Il codice sotto popolerà innerHTML. Esegui prima il codice sopra, il quale osserverà ed evidenzierà il nuovo contenuto:
let demoElem = document.getElementById('highlight-demo');
//inserisce dinamicamente del contenuto con dei code snippets
demoElem.innerHTML = `Giù c'è un code snippet:
<pre class="language-javascript"><code> let hello = "world!"; </code></pre>
<div>Un altro:</div>
<div>
<pre class="language-css"><code>.class { margin: 5px; } </code></pre>
</div>
`;
Ora abbiamo il MutationObserver che può tenere traccia di tutte le evidenzaiture negli elementi osservati oppure lâintero document. Possiamo aggiungere/rimuovere frammenti di codice nellâHTML senza doverci preoccupare di gestirli.
Metodi addizionali
Câè un metodo per interrompere lâosservazione del nodo:
-
observer.disconnect()â ferma lâosservazione. -
mutationRecords = observer.takeRecords()â ottiene una lista di mutation records non processati, quelli che sono avvenuti, ma che non sono stati gestiti dalla callback.
// ci piacerebbe smettere di tenere traccia delle variazioni
observer.disconnect();
Observers use weak references to nodes internally. That is, if a node is removed from the DOM, and becomes unreachable, then it can be garbage collected.
Gli observer utilizzano internamente dei riferimenti deboli ai nodi. Si tratta di questo: se un nodo viene rimosso dal DOM, diventando quindi irraggiungibile, allora diventa eleggibile per la garbage collection, e un observer certamente non lo previene.
## Riepilogo
I `MutationObserver` possono reagire alle variazioni nel DOM: attributi, elementi aggiunti/rimossi, contenuto testuale.
Possiamo usarli per tenere traccia di modifiche introdotte da altre parti del nostro codice, così come quando integriamo script di terze parti.
I `MutationObserver` possono tenere traccia di ogni variazione. Le opzioni di configurazione "cosa osservare" vengono utilizzate per le ottimizzazioni, non per impiegare risorse in invocazioni di callback non richieste.
Commenti
<code>, per molte righe â includile nel tag<pre>, per più di 10 righe â utilizza una sandbox (plnkr, jsbin, codepenâ¦)