Cunoașterea API-ului MutationObserver

Publicat: 2022-03-10
Rezumat rapid ↬ Monitorizarea modificărilor la DOM este uneori necesară în aplicațiile web și cadrele complexe. Prin intermediul explicațiilor împreună cu demonstrații interactive, acest articol vă va arăta cum puteți utiliza API-ul MutationObserver pentru a face observarea modificărilor DOM relativ ușoară.

În aplicațiile web complexe, modificările DOM pot fi frecvente. Ca urmare, există cazuri în care aplicația dvs. ar putea trebui să răspundă la o anumită modificare a DOM.

De ceva timp, modalitatea acceptată de a căuta modificări la DOM a fost prin intermediul unei caracteristici numite Mutation Events, care este acum depreciată. Înlocuitorul aprobat de W3C pentru Mutation Events este API-ul MutationObserver, despre care voi discuta în detaliu în acest articol.

O serie de articole și referințe mai vechi discută de ce vechea caracteristică a fost înlocuită, așa că nu voi intra în detaliu aici (în afară de faptul că nu aș putea să-i fac dreptate). API-ul MutationObserver are suport aproape complet pentru browser, așa că îl putem folosi în siguranță în majoritatea proiectelor, dacă nu în toate, dacă este nevoie.

Sintaxă de bază pentru un observator de mutații

Un MutationObserver poate fi folosit în mai multe moduri diferite, pe care le voi acoperi în detaliu în restul acestui articol, dar sintaxa de bază pentru un MutationObserver arată astfel:

 let observer = new MutationObserver(callback); function callback (mutations) { // do something here } observer.observe(targetNode, observerOptions);

Prima linie creează un nou MutationObserver folosind constructorul MutationObserver() . Argumentul transmis în constructor este o funcție de apel invers care va fi apelată la fiecare modificare DOM care se califică.

Modul de a determina ceea ce se califică pentru un anumit observator este prin intermediul liniei finale din codul de mai sus. Pe acea linie, folosesc metoda observe() a MutationObserver pentru a începe observarea. Puteți compara acest lucru cu ceva de genul addEventListener() . De îndată ce atașați un ascultător, pagina va „asculta” evenimentul specificat. În mod similar, când începeți să observați, pagina va începe „observarea” pentru MutationObserver specificat.

Mai multe după săritură! Continuați să citiți mai jos ↓

Metoda observe() are două argumente: ținta , care ar trebui să fie nodul sau arborele de noduri pe care să se observe modificări; și un obiect opțiuni , care este un obiect MutationObserverInit care vă permite să definiți configurația pentru observator.

Caracteristica de bază cheie finală a unui MutationObserver este metoda disconnect() . Acest lucru vă permite să opriți observarea modificărilor specificate și arată astfel:

 observer.disconnect();

Opțiuni pentru a configura un MutationObserver

După cum sa menționat, metoda observe() a unui MutationObserver necesită un al doilea argument care specifică opțiunile pentru a descrie MutationObserver . Iată cum ar arăta obiectul opțiuni cu toate perechile posibile de proprietate/valoare incluse:

 let options = { childList: true, attributes: true, characterData: false, subtree: false, attributeFilter: ['one', 'two'], attributeOldValue: false, characterDataOldValue: false };

Când configurați opțiunile MutationObserver , nu este necesar să includeți toate aceste linii. Le includ pur și simplu în scopuri de referință, astfel încât să puteți vedea ce opțiuni sunt disponibile și ce tipuri de valori pot lua. După cum puteți vedea, toate, cu excepția unuia, sunt booleene.

Pentru ca un MutationObserver să funcționeze, cel puțin unul dintre childList , attributes sau characterData trebuie setat la true , altfel va fi generată o eroare. Celelalte patru proprietăți funcționează împreună cu una dintre cele trei (mai multe despre asta mai târziu).

Până acum, am trecut doar peste sintaxa pentru a vă oferi o privire de ansamblu. Cel mai bun mod de a lua în considerare modul în care funcționează fiecare dintre aceste caracteristici este oferind exemple de cod și demonstrații live care încorporează diferite opțiuni. Deci asta voi face pentru restul articolului.

Observarea modificărilor la elementele copil folosind childList

Primul și cel mai simplu MutationObserver pe care îl puteți iniția este unul care caută nodurile copil ale unui nod specificat (de obicei un element) pentru a fi adăugate sau eliminate. De exemplu, voi crea o listă neordonată în HTML-ul meu și vreau să știu ori de câte ori un nod copil este adăugat sau eliminat din acest element de listă.

HTML-ul pentru listă arată astfel:

 <ul class="list"> <li>Apples</li> <li>Oranges</li> <li>Bananas</li> <li class="child">Peaches</li> </ul>

JavaScript pentru MutationObserver meu include următoarele:

 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);

Aceasta este doar o parte a codului. Pentru concizie, arăt cele mai importante secțiuni care se ocupă de API-ul MutationObserver în sine.

Observați cum parcurg argumentul mutations , care este un obiect MutationRecord care are o serie de proprietăți diferite. În acest caz, citesc proprietatea type și înregistrez un mesaj care indică faptul că browserul a detectat o mutație care se califică. De asemenea, observați cum transmit elementul mList (o referință la lista mea HTML) ca element vizat (adică elementul pe care vreau să observ pentru modificări).

  • Vedeți demonstrația interactivă completă →

Utilizați butoanele pentru a porni și opri MutationObserver . Mesajele de jurnal ajută la clarificarea a ceea ce se întâmplă. Comentariile din cod oferă, de asemenea, unele explicații.

Rețineți câteva puncte importante aici:

  • Funcția de apel invers (pe care am numit-o mCallback , pentru a ilustra faptul că o puteți numi cum doriți) se va declanșa de fiecare dată când este detectată o mutație de succes și după ce metoda observe() este executată.
  • În exemplul meu, singurul „tip” de mutație care se califică este childList , așa că este logic să o cauți atunci când o faci buclă prin MutationRecord. Căutarea oricărui alt tip în acest caz nu ar face nimic (celelalte tipuri vor fi folosite în demonstrațiile ulterioare).
  • Folosind childList , pot adăuga sau elimina un nod text din elementul vizat și și acesta s-ar califica. Deci nu trebuie să fie un element care este adăugat sau eliminat.
  • În acest exemplu, numai nodurile secundare imediate se vor califica. Mai târziu în articol, vă voi arăta cum acest lucru se poate aplica tuturor nodurilor copil, nepoților și așa mai departe.

Observarea modificărilor atributelor unui element

Un alt tip comun de mutație pe care ați putea dori să-l urmăriți este atunci când un atribut al unui element specificat se modifică. În următoarea demonstrație interactivă, voi observa modificările aduse atributelor unui element de paragraf.

 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);
  • Încercați demonstrația →

Din nou, am abreviat codul pentru claritate, dar părțile importante sunt:

  • Obiectul options folosește proprietatea attributes , setată la true pentru a spune MutationObserver că vreau să caut modificări la atributele elementului vizat.
  • Tipul de mutație pentru care testez în bucla mea este attributes , singurul care se califică în acest caz.
  • De asemenea, folosesc proprietatea attributeName a obiectului mutation , care îmi permite să aflu ce atribut a fost schimbat.
  • Când declanșez observatorul, trec elementul paragraf prin referință, împreună cu opțiunile.

În acest exemplu, un buton este folosit pentru a comuta un nume de clasă pe elementul HTML vizat. Funcția de apel invers din observatorul de mutații este declanșată de fiecare dată când clasa este adăugată sau eliminată.

Observarea modificărilor datelor de caractere

O altă modificare pe care ați putea dori să o căutați în aplicația dvs. sunt mutațiile la datele de caractere; adică modificări la un anumit nod de text. Acest lucru se face prin setarea proprietății characterData la true în obiectul options . Iată codul:

 let options = { characterData: true }, observer = new MutationObserver(mCallback); function mCallback(mutations) { for (let mutation of mutations) { if (mutation.type === 'characterData') { // Do something here... } } }

Observați din nou că type căutat în funcția de apel invers este characterData .

  • Vezi demonstrația live →

În acest exemplu, caut modificări la un anumit nod text, pe care îl țintesc prin element.childNodes[0] . Este puțin hacker, dar va fi potrivit pentru acest exemplu. Textul poate fi editat de utilizator prin atributul contenteditable al unui element de paragraf.

Provocări la observarea modificărilor datelor despre caractere

Dacă te-ai jucat cu contenteditable , atunci s-ar putea să știi că există comenzi rapide de la tastatură care permit editarea textului îmbogățit. De exemplu, CTRL-B face textul aldine, CTRL-I face textul italic și așa mai departe. Acest lucru va împărți nodul de text în mai multe noduri de text, așa că veți observa că MutationObserver nu va mai răspunde decât dacă editați textul care este încă considerat parte a nodului original.

De asemenea, ar trebui să subliniez că dacă ștergeți tot textul, MutationObserver nu va mai declanșa apelul înapoi. Presupun că acest lucru se întâmplă deoarece odată ce nodul text dispare, elementul țintă nu mai există. Pentru a combate acest lucru, demonstrația mea nu mai observă când textul este eliminat, deși lucrurile devin puțin lipicioase atunci când utilizați comenzi rapide pentru text îmbogățit.

Dar nu vă faceți griji, mai târziu în acest articol, voi discuta despre o modalitate mai bună de a folosi opțiunea characterData fără a fi nevoit să se ocupe de cât mai multe dintre aceste ciudatenii.

Observarea modificărilor aduse atributelor specificate

Mai devreme v-am arătat cum să observați modificările aduse atributelor pe un element specificat. În acest caz, deși demonstrația declanșează o schimbare a numelui clasei, aș fi putut schimba orice atribut al elementului specificat. Dar ce se întâmplă dacă vreau să observ modificări ale unuia sau mai multor atribute specifice în timp ce le ignor pe celelalte?

Pot face asta folosind proprietatea opțională attributeFilter din obiectul option . Iată un exemplu:

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

După cum se arată mai sus, proprietatea attributeFilter acceptă o serie de atribute specifice pe care vreau să le monitorizez. În acest exemplu, MutationObserver va declanșa apelul invers de fiecare dată când unul sau mai multe dintre atributele hidden , contenteditable prin conținut sau data-par sunt modificate.

  • Vezi demonstrația live →

Din nou, vizez un anumit element de paragraf. Observați meniul derulant de selectare care alege ce atribut va fi modificat. Atributul draggable este singurul care nu se califică, deoarece nu l-am specificat pe acesta în opțiunile mele.

Observați în cod că folosesc din nou proprietatea attributeName a obiectului MutationRecord pentru a înregistra ce atribut a fost schimbat. Și, desigur, ca și în cazul celorlalte demonstrații, MutationObserver nu va începe să monitorizeze modificările până când nu se face clic pe butonul „start”.

Un alt lucru pe care ar trebui să-l subliniez aici este că nu trebuie să setez valoarea attributes la true în acest caz; este subînțeles datorită faptului că attributesFilter este setat la adevărat. De aceea, obiectul meu de opțiuni ar putea arăta după cum urmează și ar funcționa la fel:

 let options = { attributeFilter: ['hidden', 'contenteditable', 'data-par'] }

Pe de altă parte, dacă aș seta în mod explicit attributes la false împreună cu o matrice attributeFilter , nu ar funcționa, deoarece valoarea false ar avea prioritate și opțiunea de filtru ar fi ignorată.

Observarea modificărilor nodurilor și sub-arborelui acestora

Până acum, când am configurat fiecare MutationObserver , am avut de-a face doar cu elementul vizat în sine și, în cazul childList , cu copiii imediati ai elementului. Dar cu siguranță ar putea exista un caz în care aș dori să observ modificări la una dintre următoarele:

  • Un element și toate elementele sale fii;
  • Unul sau mai multe atribute pe un element și pe elementele sale fii;
  • Toate nodurile text din interiorul unui element.

Toate cele de mai sus pot fi realizate folosind proprietatea subtree a obiectului opțiuni.

childList Cu subtree

Mai întâi, să căutăm modificări la nodurile copil ale unui element, chiar dacă nu sunt copii imediati. Îmi pot modifica obiectul de opțiuni pentru a arăta astfel:

 options = { childList: true, subtree: true }

Orice altceva din cod este mai mult sau mai puțin la fel cu exemplul precedent childList , împreună cu unele markupuri și butoane suplimentare.

  • Vezi demonstrația live →

Aici există două liste, una imbricată în cealaltă. Când MutationObserver este pornit, apelul invers va declanșa modificări la oricare dintre liste. Dar dacă ar fi să schimb proprietatea subtree înapoi la false (implicit atunci când nu este prezent), callback-ul nu s-ar executa când lista imbricată este modificată.

Atribute Cu subarbore

Iată un alt exemplu, de data aceasta folosind subtree cu attributes și attributeFilter . Acest lucru îmi permite să observ modificări ale atributelor nu numai ale elementului țintă, ci și ale atributelor oricăror elemente secundare ale elementului țintă:

 options = { attributes: true, attributeFilter: ['hidden', 'contenteditable', 'data-par'], subtree: true }
  • Vezi demonstrația live →

Aceasta este similară cu demo-ul anterioară de atribute, dar de data aceasta am configurat două elemente selectate diferite. Primul modifică atributele elementului de paragraf vizat, în timp ce celălalt modifică atributele unui element copil din interiorul paragrafului.

Din nou, dacă ar fi să setați opțiunea subtree înapoi la false (sau să o eliminați), al doilea buton de comutare nu ar declanșa apelul invers MutationObserver . Și, desigur, aș putea omite attributeFilter cu totul, iar MutationObserver ar căuta modificări ale oricăror atribute din subarborele, mai degrabă decât ale celor specificate.

characterData Cu subtree

Amintiți-vă, în demonstrația anterioară a characterData Data, au existat câteva probleme cu dispariția nodului vizat și apoi MutationObserver nu mai funcționează. Deși există modalități de a ocoli acest lucru, este mai ușor să vizați un element direct, mai degrabă decât un nod text, apoi utilizați proprietatea subtree pentru a specifica că vreau ca toate datele caracterelor din interiorul acelui element, indiferent cât de adânc sunt imbricate, să se declanșeze apel invers MutationObserver .

Opțiunile mele în acest caz ar arăta astfel:

 options = { characterData: true, subtree: true }
  • Vezi demonstrația live →

După ce porniți observatorul, încercați să utilizați CTRL-B și CTRL-I pentru a formata textul editabil. Veți observa că acest lucru funcționează mult mai eficient decât exemplul anterior characterData . În acest caz, nodurile copil rupte nu afectează observatorul, deoarece observăm toate nodurile din interiorul nodului vizat, în loc de un singur nod text.

Înregistrarea valorilor vechi

Adesea, atunci când observați modificări ale DOM-ului, veți dori să luați notă de vechile valori și, eventual, să le stocați sau să le utilizați în altă parte. Acest lucru se poate face folosind câteva proprietăți diferite din obiectul options .

attributeOldValue

În primul rând, să încercăm să deconectam vechea valoare a atributului după ce a fost schimbată. Iată cum vor arăta opțiunile mele împreună cu apelul înapoi:

 options = { attributes: true, attributeOldValue: true } function mCallback (mutations) { for (let mutation of mutations) { if (mutation.type === 'attributes') { // Do something here... } } }
  • Vezi demonstrația live →

Observați utilizarea proprietăților attributeName și oldValue ale obiectului MutationRecord . Încercați demonstrația introducând diferite valori în câmpul de text. Observați cum se actualizează jurnalul pentru a reflecta valoarea anterioară care a fost stocată.

characterDataOldValue

În mod similar, iată cum ar arăta opțiunile mele dacă vreau să înregistrez datele vechi de caractere:

 options = { characterData: true, subtree: true, characterDataOldValue: true }
  • Vezi demonstrația live →

Observați că mesajele de jurnal indică valoarea anterioară. Lucrurile devin puțin ciudate când adăugați HTML prin comenzi de text îmbogățit la mix. Nu sunt sigur care ar trebui să fie comportamentul corect în acest caz, dar este mai simplu dacă singurul lucru din interiorul elementului este un singur nod text.

Interceptarea mutațiilor folosind takeRecords()

O altă metodă a obiectului MutationObserver pe care nu am menționat-o încă este takeRecords() . Această metodă vă permite să interceptați mai mult sau mai puțin mutațiile care sunt detectate înainte de a fi procesate de funcția de apel invers.

Pot folosi această caracteristică folosind o linie ca aceasta:

 let myRecords = observer.takeRecords();

Aceasta stochează o listă a modificărilor DOM în variabila specificată. În demonstrația mea, execut această comandă de îndată ce se face clic pe butonul care modifică DOM. Observați că butoanele de pornire și adăugare/eliminare nu înregistrează nimic. Acest lucru se datorează faptului că, după cum am menționat, interceptez modificările DOM înainte ca acestea să fie procesate de apel invers.

Observați, totuși, ce fac eu în ascultătorul de evenimente care îl oprește pe observator:

 btnStop.addEventListener('click', function () { observer.disconnect(); if (myRecords) { console.log(`${myRecords[0].target} was changed using the ${myRecords[0].type} option.`); } }, false);

După cum puteți vedea, după oprirea observatorului folosind observer.disconnect() , accesez înregistrarea mutației care a fost interceptată și înregistrez elementul țintă, precum și tipul de mutație care a fost înregistrat. Dacă aș fi observat mai multe tipuri de modificări, atunci înregistrarea stocată ar avea mai mult de un articol în ea, fiecare cu propriul său tip.

Când o înregistrare de mutație este interceptată în acest mod prin apelarea takeRecords() , coada de mutații care ar fi în mod normal trimisă la funcția de apel invers este golită. Deci, dacă dintr-un motiv oarecare trebuie să interceptați aceste înregistrări înainte de a fi procesate, takeRecords() ar fi util.

Observarea modificărilor multiple folosind un singur observator

Rețineți că, dacă caut mutații pe două noduri diferite de pe pagină, pot face acest lucru folosind același observator. Aceasta înseamnă că după ce apelez constructorul, pot executa metoda observe() pentru câte elemente vreau.

Astfel, după această linie:

 observer = new MutationObserver(mCallback);

Apoi pot avea mai multe apeluri observe() cu elemente diferite ca prim argument:

 observer.observe(mList, options); observer.observe(mList2, options);
  • Vezi demonstrația live →

Porniți observatorul, apoi încercați butoanele de adăugare/eliminare pentru ambele liste. Singura captură aici este că, dacă apăsați pe unul dintre butoanele de „oprire”, observatorul va opri observarea pentru ambele liste, nu doar pentru cea pe care o vizează.

Mutarea unui arbore de noduri care este observat

Un ultim lucru pe care îl voi sublinia este că un MutationObserver va continua să observe modificările aduse unui nod specificat chiar și după ce acel nod a fost eliminat din elementul său părinte.

De exemplu, încercați următoarea demonstrație:

  • Vezi demonstrația live →

Acesta este un alt exemplu care folosește childList pentru a monitoriza modificările aduse elementelor copil ale unui element țintă. Observați butonul care deconectează sub-lista, care este cea care este observată. Faceți clic pe butonul „Start…”, apoi faceți clic pe butonul „Mutați...” pentru a muta lista imbricată. Chiar și după ce lista este eliminată din părintele ei, MutationObserver continuă să observe modificările specificate. Nu este o surpriză majoră că se întâmplă acest lucru, dar este ceva de reținut.

Concluzie

Aceasta acoperă aproape toate caracteristicile principale ale API-ului MutationObserver . Sper că această scufundare profundă v-a fost utilă pentru a vă familiariza cu acest standard. După cum am menționat, suportul pentru browser este puternic și puteți citi mai multe despre acest API pe paginile MDN.

Am pus toate demo-urile pentru acest articol într-o colecție CodePen, dacă doriți să aveți un loc ușor de jucat cu demo-urile.