Conoscere l'API MutationObserver
Pubblicato: 2022-03-10Nelle app Web complesse, le modifiche al DOM possono essere frequenti. Di conseguenza, ci sono casi in cui la tua app potrebbe dover rispondere a una specifica modifica al DOM.
Per qualche tempo, il modo accettato per cercare le modifiche al DOM era per mezzo di una funzionalità chiamata Mutation Events, che ora è deprecata. La sostituzione approvata dal W3C per gli eventi di mutazione è l'API MutationObserver, che è ciò di cui parlerò in dettaglio in questo articolo.
Numerosi articoli e riferimenti precedenti discutono del motivo per cui la vecchia funzionalità è stata sostituita, quindi non entrerò nei dettagli su questo qui (oltre al fatto che non sarei in grado di rendergli giustizia). L'API MutationObserver
ha un supporto per browser quasi completo, quindi possiamo utilizzarla in sicurezza nella maggior parte, se non in tutti, i progetti, in caso di necessità.
Sintassi di base per un osservatore di mutazioni
Un MutationObserver
può essere utilizzato in diversi modi, che tratterò in dettaglio nel resto di questo articolo, ma la sintassi di base per un MutationObserver
è simile a questa:
let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);
La prima riga crea un nuovo MutationObserver
utilizzando il costruttore MutationObserver()
. L'argomento passato al costruttore è una funzione di callback che verrà chiamata a ogni modifica DOM qualificata.
Il modo per determinare cosa si qualifica per un particolare osservatore è per mezzo della riga finale nel codice sopra. Su quella riga, sto usando il metodo observe()
di MutationObserver
per iniziare a osservare. Puoi confrontarlo con qualcosa come addEventListener()
. Non appena colleghi un listener, la pagina "ascolterà" l'evento specificato. Allo stesso modo, quando inizi a osservare, la pagina inizierà a "osservare" per il MutationObserver
specificato.
Il metodo observe()
accetta due argomenti: il target , che dovrebbe essere il nodo o l'albero dei nodi su cui osservare le modifiche; e un oggetto options , che è un oggetto MutationObserverInit
che consente di definire la configurazione per l'osservatore.
L'ultima caratteristica fondamentale di un MutationObserver
è il metodo disconnect()
. Ciò ti consente di interrompere l'osservazione per le modifiche specificate e si presenta così:
observer.disconnect();
Opzioni per configurare un MutationObserver
Come accennato, il metodo observe()
di un MutationObserver
richiede un secondo argomento che specifichi le opzioni per descrivere MutationObserver
. Ecco come apparirà l'oggetto delle opzioni con tutte le possibili coppie proprietà/valore incluse:
let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };
Quando si impostano le opzioni di MutationObserver
, non è necessario includere tutte queste righe. Li includo semplicemente a scopo di riferimento, così puoi vedere quali opzioni sono disponibili e quali tipi di valori possono assumere. Come puoi vedere, tutti tranne uno sono booleani.
Affinché un MutationObserver
funzioni, almeno uno di childList
, attributes
o characterData
deve essere impostato su true
, altrimenti verrà generato un errore. Le altre quattro proprietà funzionano insieme a una di queste tre (ne parleremo più avanti).
Finora ho semplicemente sorvolato sulla sintassi per darti una panoramica. Il modo migliore per considerare come funziona ciascuna di queste funzionalità è fornire esempi di codice e demo dal vivo che incorporano le diverse opzioni. Quindi è quello che farò per il resto di questo articolo.
Osservare le modifiche agli elementi figlio utilizzando childList
Il primo e più semplice MutationObserver
che puoi avviare è quello che cerca i nodi figli di un nodo specificato (di solito un elemento) da aggiungere o rimuovere. Per il mio esempio, creerò un elenco non ordinato nel mio HTML e voglio sapere ogni volta che un nodo figlio viene aggiunto o rimosso da questo elemento dell'elenco.
L'HTML per l'elenco è simile al seguente:
<ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>
Il JavaScript per il mio MutationObserver
include quanto segue:
let mList = document.getElementById('myList'), options = { childList: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'childList') { console.log('Mutation Detected: A child node has been added or removed.'); } } } observer.observe(mList, options);
Questa è solo una parte del codice. Per brevità, sto mostrando le sezioni più importanti che riguardano l'API MutationObserver
stessa.
Nota come sto scorrendo l'argomento delle mutations
, che è un oggetto MutationRecord
che ha un numero di proprietà diverse. In questo caso, sto leggendo la proprietà type
e sto registrando un messaggio che indica che il browser ha rilevato una mutazione che si qualifica. Inoltre, nota come sto passando l'elemento mList
(un riferimento al mio elenco HTML) come elemento di destinazione (cioè l'elemento su cui voglio osservare le modifiche).
- Guarda la demo interattiva completa →
Utilizzare i pulsanti per avviare e arrestare MutationObserver
. I messaggi di registro aiutano a chiarire cosa sta succedendo. I commenti nel codice forniscono anche alcune spiegazioni.
Nota alcuni punti importanti qui:
- La funzione di callback (che ho chiamato
mCallback
, per illustrare che puoi nominarla come vuoi) si attiverà ogni volta che viene rilevata una mutazione riuscita e dopo che il metodoobserve()
viene eseguito. - Nel mio esempio, l'unico "tipo" di mutazione che si qualifica è
childList
, quindi ha senso cercare questo quando si scorre MutationRecord. Cercare qualsiasi altro tipo in questo caso non servirebbe a nulla (gli altri tipi verranno utilizzati nelle demo successive). - Usando
childList
, posso aggiungere o rimuovere un nodo di testo dall'elemento di destinazione e anche questo si qualificherebbe. Quindi non deve essere un elemento che viene aggiunto o rimosso. - In questo esempio, si qualificheranno solo i nodi figlio immediati. Più avanti nell'articolo, ti mostrerò come questo può essere applicato a tutti i nodi figli, ai nipoti e così via.
Osservando le modifiche agli attributi di un elemento
Un altro tipo comune di mutazione che potresti voler monitorare è quando un attributo su un elemento specificato cambia. Nella prossima demo interattiva, osserverò le modifiche agli attributi su un elemento di paragrafo.
let mPar = document.getElementById('myParagraph'), options = { attributes: true }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } } observer.observe(mPar, options);
- Prova la demo →
Ancora una volta, ho abbreviato il codice per chiarezza, ma le parti importanti sono:
- L'oggetto
options
sta usando la proprietà degliattributes
, impostata sutrue
per dire aMutationObserver
che voglio cercare le modifiche agli attributi dell'elemento di destinazione. - Il tipo di mutazione che sto testando nel mio ciclo è
attributes
, l'unico che si qualifica in questo caso. - Sto anche usando la proprietà
attributeName
dell'oggettomutation
, che mi consente di scoprire quale attributo è stato modificato. - Quando attivo l'osservatore, passo l'elemento paragrafo per riferimento, insieme alle opzioni.
In questo esempio, viene utilizzato un pulsante per attivare o disattivare il nome di una classe sull'elemento HTML di destinazione. La funzione di callback nell'osservatore di mutazione viene attivata ogni volta che la classe viene aggiunta o rimossa.
Osservando le modifiche ai dati del personaggio
Un'altra modifica che potresti voler cercare nella tua app sono le mutazioni nei dati dei personaggi; ovvero, le modifiche a un nodo di testo specifico. Questo viene fatto impostando la proprietà characterData
su true
nell'oggetto options
. Ecco il codice:
let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }
Si noti ancora che il type
cercato nella funzione di callback è characterData
.
- Guarda la demo dal vivo →
In questo esempio, sto cercando modifiche a un nodo di testo specifico, che ho scelto come target tramite element.childNodes[0]
. Questo è un po 'hacky ma andrà bene per questo esempio. Il testo è modificabile dall'utente tramite l'attributo contenteditable
su un elemento paragrafo.
Sfide quando si osservano le modifiche ai dati del personaggio
Se hai armeggiato con contenteditable
, potresti essere consapevole che esistono scorciatoie da tastiera che consentono la modifica del testo RTF. Ad esempio, CTRL-B rende il testo in grassetto, CTRL-I rende il testo in corsivo e così via. Ciò suddividerà il nodo di testo in più nodi di testo, quindi noterai che MutationObserver
smetterà di rispondere a meno che tu non modifichi il testo che è ancora considerato parte del nodo originale.
Dovrei anche sottolineare che se elimini tutto il testo, MutationObserver
non attiverà più il callback. Presumo che ciò accada perché una volta che il nodo di testo scompare, l'elemento di destinazione non esiste più. Per combattere questo, la mia demo smette di osservare quando il testo viene rimosso, anche se le cose diventano un po' appiccicose quando usi le scorciatoie RTF.
Ma non preoccuparti, più avanti in questo articolo parlerò di un modo migliore per utilizzare l'opzione characterData
senza dover affrontare tante di queste stranezze.
Osservando le modifiche agli attributi specificati
In precedenza ti ho mostrato come osservare le modifiche agli attributi su un elemento specificato. In tal caso, sebbene la demo attivi una modifica del nome della classe, avrei potuto modificare qualsiasi attributo sull'elemento specificato. Ma cosa succede se voglio osservare le modifiche a uno o più attributi specifici ignorando gli altri?
Posso farlo usando la proprietà facoltativa attributeFilter
nell'oggetto option
. Ecco un esempio:
let options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'] }, observer = new MutationObserver(mCallback); function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
Come mostrato sopra, la proprietà attributeFilter
accetta una matrice di attributi specifici che voglio monitorare. In questo esempio, MutationObserver
attiverà il callback ogni volta che uno o più degli attributi hidden
, contenteditable
o data-par
vengono modificati.
- Guarda la demo dal vivo →
Ancora una volta sto prendendo di mira un elemento di paragrafo specifico. Notare il menu a discesa di selezione che sceglie quale attributo verrà modificato. L'attributo draggable
è l'unico che non si qualificherà poiché non l'ho specificato nelle mie opzioni.
Si noti nel codice che sto usando di nuovo la proprietà attributeName
dell'oggetto MutationRecord
per registrare quale attributo è stato modificato. E ovviamente, come con le altre demo, MutationObserver
non inizierà a monitorare le modifiche fino a quando non verrà cliccato il pulsante "Start".
Un'altra cosa che dovrei sottolineare qui è che non ho bisogno di impostare il valore degli attributes
su true
in questo caso; è implicito perché attributesFilter
è impostato su true. Ecco perché il mio oggetto opzioni potrebbe apparire come segue e funzionerebbe allo stesso modo:
let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }
D'altra parte, se imposto esplicitamente gli attributes
su false
insieme a un array attributeFilter
, non funzionerebbe perché il valore false
avrebbe la precedenza e l'opzione del filtro verrebbe ignorata.
Osservando le modifiche ai nodi e al loro sottoalbero
Finora, durante la configurazione di ogni MutationObserver
, mi sono occupato solo dell'elemento di destinazione stesso e, nel caso di childList
, dei figli immediati dell'elemento. Ma sicuramente potrebbe esserci un caso in cui potrei voler osservare le modifiche a uno dei seguenti:
- Un elemento e tutti i suoi elementi figlio;
- Uno o più attributi su un elemento e sui suoi elementi figlio;
- Tutti i nodi di testo all'interno di un elemento.
Tutto quanto sopra può essere ottenuto utilizzando la proprietà subtree
dell'oggetto options.
childList Con sottoalbero
Per prima cosa, cerchiamo le modifiche ai nodi figlio di un elemento, anche se non sono figli immediati. Posso modificare il mio oggetto opzioni in modo che assomigli a questo:
options = { childList: true, subtree: true }
Tutto il resto nel codice è più o meno lo stesso dell'esempio childList
precedente, insieme ad alcuni pulsanti e markup aggiuntivi.
- Guarda la demo dal vivo →
Qui ci sono due elenchi, uno annidato dentro l'altro. Quando MutationObserver
viene avviato, il callback attiverà le modifiche a uno degli elenchi. Ma se dovessi riportare la proprietà del subtree
su false
(l'impostazione predefinita quando non è presente), il callback non verrebbe eseguito quando l'elenco nidificato viene modificato.
Attributi Con sottoalbero
Ecco un altro esempio, questa volta utilizzando subtree
con attributes
e attributeFilter
. Questo mi permette di osservare le modifiche agli attributi non solo sull'elemento di destinazione ma anche sugli attributi di qualsiasi elemento figlio dell'elemento di destinazione:
options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
- Guarda la demo dal vivo →
È simile alla precedente demo degli attributi, ma questa volta ho impostato due diversi elementi di selezione. Il primo modifica gli attributi sull'elemento paragrafo di destinazione mentre l'altro modifica gli attributi su un elemento figlio all'interno del paragrafo.
Di nuovo, se dovessi reimpostare l'opzione del subtree
su false
(o rimuoverla), il secondo pulsante di attivazione/disattivazione non attiverebbe il callback di MutationObserver
. E, naturalmente, potrei omettere del tutto attributeFilter
e MutationObserver
cercherà le modifiche a qualsiasi attributo nel sottoalbero anziché a quelli specificati.
characterData Con sottoalbero
Ricorda nella precedente demo di characterData
, c'erano alcuni problemi con la scomparsa del nodo di destinazione e quindi il MutationObserver
non funzionava più. Sebbene ci siano modi per aggirare il problema, è più facile scegliere come target un elemento direttamente piuttosto che un nodo di testo, quindi utilizzare la proprietà subtree
per specificare che voglio che tutti i dati del carattere all'interno di quell'elemento, non importa quanto sia profondamente nidificato, per attivare la richiamata di MutationObserver
.
Le mie opzioni in questo caso sarebbero simili a questa:
options = { characterData: true, subtree: true }
- Guarda la demo dal vivo →
Dopo aver avviato l'osservatore, prova a utilizzare CTRL-B e CTRL-I per formattare il testo modificabile. Noterai che funziona in modo molto più efficace rispetto al precedente esempio characterData
. In questo caso, i nodi figlio suddivisi non influiscono sull'osservatore perché stiamo osservando tutti i nodi all'interno del nodo di destinazione, invece di un singolo nodo di testo.
Registrazione di vecchi valori
Spesso quando osservi le modifiche al DOM, ti consigliamo di prendere nota dei vecchi valori ed eventualmente archiviarli o usarli altrove. Questo può essere fatto usando alcune proprietà diverse nell'oggetto options
.
attributoOldValue
Innanzitutto, proviamo a disconnettere il vecchio valore dell'attributo dopo che è stato modificato. Ecco come appariranno le mie opzioni insieme alla mia richiamata:
options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
- Guarda la demo dal vivo →
Si noti l'uso delle proprietà attributeName
e oldValue
dell'oggetto MutationRecord
. Prova la demo inserendo diversi valori nel campo di testo. Nota come il registro si aggiorna per riflettere il valore precedente che è stato archiviato.
carattereDataOldValue
Allo stesso modo, ecco come apparirebbero le mie opzioni se volessi registrare i vecchi dati dei personaggi:
options = { characterData: true, subtree: true, characterDataOldValue: true }
- Guarda la demo dal vivo →
Si noti che i messaggi di registro indicano il valore precedente. Le cose diventano un po' traballanti quando aggiungi HTML tramite comandi RTF al mix. Non sono sicuro di quale dovrebbe essere il comportamento corretto in quel caso, ma è più semplice se l'unica cosa all'interno dell'elemento è un singolo nodo di testo.
Intercettare le mutazioni usando takeRecords()
Un altro metodo dell'oggetto MutationObserver
che non ho ancora menzionato è takeRecords()
. Questo metodo consente di intercettare più o meno le mutazioni rilevate prima che vengano elaborate dalla funzione di callback.
Posso usare questa funzione usando una linea come questa:
let myRecords = observer.takeRecords();
Questo memorizza un elenco delle modifiche DOM nella variabile specificata. Nella mia demo, sto eseguendo questo comando non appena viene cliccato il pulsante che modifica il DOM. Si noti che i pulsanti di avvio e di aggiunta/rimozione non registrano nulla. Questo perché, come accennato, sto intercettando le modifiche DOM prima che vengano elaborate dal callback.
Nota, tuttavia, cosa sto facendo nell'event listener che interrompe l'osservatore:
btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);
Come puoi vedere, dopo aver fermato l'osservatore usando observer.disconnect()
, sto accedendo al record di mutazione che è stato intercettato e sto registrando l'elemento target e il tipo di mutazione che è stato registrato. Se avessi osservato più tipi di modifiche, il record archiviato conterrebbe più di un elemento, ognuno con il proprio tipo.
Quando un record di mutazione viene intercettato in questo modo chiamando takeRecords()
, la coda di mutazioni che normalmente verrebbe inviata alla funzione di callback viene svuotata. Quindi, se per qualche motivo è necessario intercettare questi record prima che vengano elaborati, takeRecords()
sarebbe utile.
Osservazione di più modifiche utilizzando un unico osservatore
Nota che se sto cercando mutazioni su due nodi diversi nella pagina, posso farlo usando lo stesso osservatore. Ciò significa che dopo aver chiamato il costruttore, posso eseguire il metodo observe()
per tutti gli elementi che voglio.
Quindi, dopo questa riga:
observer = new MutationObserver(mCallback);
Posso quindi avere più chiamate observe()
con elementi diversi come primo argomento:
observer.observe(mList, options); observer.observe(mList2, options);
- Guarda la demo dal vivo →
Avvia l'osservatore, quindi prova i pulsanti aggiungi/rimuovi per entrambi gli elenchi. L'unico problema qui è che se si preme uno dei pulsanti "stop", l'osservatore smetterà di osservare per entrambi gli elenchi, non solo per quello che sta prendendo di mira.
Spostamento di un albero di nodi che viene osservato
Un'ultima cosa che sottolineerò è che un MutationObserver
continuerà a osservare le modifiche a un nodo specificato anche dopo che quel nodo è stato rimosso dal suo elemento padre.
Ad esempio, prova la seguente demo:
- Guarda la demo dal vivo →
Questo è un altro esempio che usa childList
per monitorare le modifiche agli elementi figlio di un elemento di destinazione. Si noti il pulsante che disconnette la sotto-lista, che è quella osservata. Fare clic sul pulsante "Inizia...", quindi fare clic sul pulsante "Sposta..." per spostare l'elenco nidificato. Anche dopo che l'elenco è stato rimosso dal padre, MutationObserver
continua a osservare le modifiche specificate. Non è una grande sorpresa che ciò accada, ma è qualcosa da tenere a mente.
Conclusione
Ciò copre quasi tutte le funzionalità principali dell'API MutationObserver
. Spero che questa immersione profonda ti sia stata utile per familiarizzare con questo standard. Come accennato, il supporto del browser è forte e puoi leggere di più su questa API sulle pagine di MDN.
Ho inserito tutte le demo per questo articolo in una raccolta CodePen, se vuoi avere un posto facile per scherzare con le demo.