Cunoașterea API-ului MutationObserver
Publicat: 2022-03-10Î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.
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 metodaobserve()
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 proprietateaattributes
, setată latrue
pentru a spuneMutationObserver
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 obiectuluimutation
, 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.