Conoscere l'API MutationObserver

Pubblicato: 2022-03-10
Riepilogo rapido ↬ Il monitoraggio delle modifiche al DOM è talvolta necessario in app Web e framework complessi. Per mezzo di spiegazioni e demo interattive, questo articolo ti mostrerà come utilizzare l'API MutationObserver per rendere relativamente facile l'osservazione delle modifiche DOM.

Nelle 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.

Altro dopo il salto! Continua a leggere sotto ↓

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 metodo observe() 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à degli attributes , impostata su true per dire a MutationObserver 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'oggetto mutation , 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.