Pozorovatel mutace

MutationObserver je vestavěný objekt, který sleduje prvek DOM a spustí zpětné volání, když detekuje změnu.

Nejprve se podíváme na syntaxi a poté prozkoumáme případ použití v reálném světě, abychom zjistili, kde může být taková věc užitečná.

Syntaxe

MutationObserver se snadno používá.

Nejprve vytvoříme pozorovatele s funkcí zpětného volání:

let observer = new MutationObserver(callback);

A pak jej připojte k uzlu DOM:

observer.observe(node, config);

config je objekt s booleovskými možnostmi „na jaký druh změn reagovat“:

  • childList – změny v přímých potomcích node ,
  • subtree – ve všech potomcích node ,
  • attributes – atributy node ,
  • attributeFilter – pole názvů atributů pro sledování pouze vybraných.
  • characterData – zda ​​dodržovat node.data (textový obsah),

Několik dalších možností:

  • attributeOldValue – pokud true , předejte zpětnému volání starou i novou hodnotu atributu (viz níže), jinak pouze novou (potřebuje attributes možnost),
  • characterDataOldValue – pokud true , předejte starou i novou hodnotu node.data zpětnému volání (viz níže), jinak pouze novému (potřebuje characterData možnost).

Po jakýchkoli změnách pak callback se provede:změny jsou předány v prvním argumentu jako seznam objektů MutationRecord a samotný pozorovatel jako druhý argument.

Objekty MutationRecord mají vlastnosti:

  • type – typ mutace, jeden z
    • "attributes" :atribut upraven
    • "characterData" :upravená data, použitá pro textové uzly,
    • "childList" :přidané/odebrané podřízené prvky,
  • target – kde ke změně došlo:prvek pro "attributes" , nebo textový uzel pro "characterData" , nebo prvek pro "childList" mutace,
  • addedNodes/removedNodes – uzly, které byly přidány/odebrány,
  • previousSibling/nextSibling – předchozího a dalšího sourozence přidaných/odebraných uzlů,
  • attributeName/attributeNamespace – název/jmenný prostor (pro XML) změněného atributu,
  • oldValue – předchozí hodnota, pouze pro změny atributu nebo textu, pokud je odpovídající volba nastavena attributeOldValue /characterDataOldValue .

Zde je například <div> s contentEditable atribut. Tento atribut nám umožňuje zaměřit se na něj a upravovat.

<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
 console.log(mutationRecords); // console.log(the changes)
});

// 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>

Pokud tento kód spustíme v prohlížeči, pak se zaměříme na daný <div> a změňte text uvnitř <b>edit</b> , console.log zobrazí jednu mutaci:

mutationRecords = [{
 type: "characterData",
 oldValue: "edit",
 target: <text node>,
 // other properties empty
}];

Provádíme-li složitější editační operace, např. odeberte <b>edit</b> , událost mutace může obsahovat více záznamů mutace:

mutationRecords = [{
 type: "childList",
 target: <div#elem>,
 removedNodes: [<b>],
 nextSibling: <text node>,
 previousSibling: <text node>
 // other properties empty
}, {
 type: "characterData"
 target: <text node>
 // ...mutation details depend on how the browser handles such removal
 // it may coalesce two adjacent text nodes "edit " and ", please" into one node
 // or it may leave them separate text nodes
}];

Takže MutationObserver umožňuje reagovat na jakékoli změny v podstromu DOM.

Použití pro integraci

Kdy se taková věc může hodit?

Představte si situaci, kdy potřebujete přidat skript třetí strany, který obsahuje užitečnou funkcionalitu, ale zároveň dělá něco nechtěného, ​​např. zobrazuje reklamy <div class="ads">Unwanted ads</div> .

Skript třetí strany přirozeně neposkytuje žádné mechanismy k jeho odstranění.

Pomocí MutationObserver , můžeme zjistit, kdy se nežádoucí prvek objeví v našem DOM, a odstranit jej.

Existují i ​​jiné situace, kdy skript třetí strany přidá něco do našeho dokumentu a my bychom chtěli zjistit, když se to stane, přizpůsobit naši stránku, dynamicky změnit velikost něčeho atd.

MutationObserver umožňuje to implementovat.

Použití pro architekturu

Existují také situace, kdy MutationObserver je dobrý z architektonického hlediska.

Řekněme, že vytváříme web o programování. Články a další materiály mohou přirozeně obsahovat úryvky zdrojového kódu.

Takový úryvek ve značce HTML vypadá takto:

...
<pre class="language-javascript"><code>
 // here's the code
 let hello = "world";
</code></pre>
...

Pro lepší čitelnost a zároveň pro zkrášlení budeme na našem webu používat knihovnu pro zvýrazňování syntaxe JavaScriptu, jako je Prism.js. Chcete-li získat zvýraznění syntaxe pro výše uvedený fragment v Prism, Prism.highlightElem(pre) je voláno, které prozkoumá obsah takového pre prvků a přidává do těchto prvků speciální značky a styly pro barevné zvýraznění syntaxe, podobné tomu, co vidíte v příkladech zde na této stránce.

Kdy přesně bychom měli spustit tuto metodu zvýraznění? No, můžeme to udělat na DOMContentLoaded událost nebo umístěte skript na konec stránky. Ve chvíli, kdy je náš DOM připraven, můžeme hledat prvky pre[class*="language"] a zavolejte Prism.highlightElem na nich:

// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

Vše je zatím jednoduché, že? Najdeme úryvky kódu v HTML a zvýrazníme je.

Nyní pojďme dál. Řekněme, že budeme dynamicky načítat materiály ze serveru. Metody pro to prostudujeme později v tutoriálu. Prozatím záleží pouze na tom, abychom načetli HTML článek z webového serveru a zobrazili jej na vyžádání:

let article = /* fetch new content from server */
articleElem.innerHTML = article;

Nový article HTML může obsahovat úryvky kódu. Musíme zavolat Prism.highlightElem na nich, jinak nebudou zvýrazněny.

Kde a kdy volat na číslo Prism.highlightElem pro dynamicky načítaný článek?

Toto volání bychom mohli připojit ke kódu, který načte článek, takto:

let article = /* fetch new content from server */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

…Ale představte si, že máme v kódu mnoho míst, kam načítáme náš obsah – články, kvízy, příspěvky na fóru atd. Potřebujeme zvýraznění všude umístit, abychom po načtení zvýraznili kód v obsahu? To není příliš pohodlné.

A co když je obsah načten modulem třetí strany? Máme například fórum napsané někým jiným, které dynamicky načítá obsah, a rádi bychom do něj přidali zvýraznění syntaxe. Nikdo nemá rád záplatování skriptů třetích stran.

Naštěstí je tu ještě jedna možnost.

Můžeme použít MutationObserver automaticky detekovat, kdy jsou na stránku vloženy úryvky kódu, a zvýraznit je.

Funkci zvýraznění tedy zvládneme na jednom místě, což nás zbaví potřeby je integrovat.

Dynamické zvýraznění ukázky

Zde je pracovní příklad.

Pokud tento kód spustíte, začne sledovat níže uvedený prvek a zvýrazňovat všechny úryvky kódu, které se tam objevují:

let observer = new MutationObserver(mutations => {

 for(let mutation of mutations) {
 // examine new nodes, is there anything to highlight?

 for(let node of mutation.addedNodes) {
 // we track only elements, skip other nodes (e.g. text nodes)
 if (!(node instanceof HTMLElement)) continue;

 // check the inserted element for being a code snippet
 if (node.matches('pre[class*="language-"]')) {
 Prism.highlightElement(node);
 }

 // or maybe there's a code snippet somewhere in its subtree?
 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});

Níže je uveden prvek HTML a JavaScript, který jej dynamicky vyplňuje pomocí innerHTML .

Spusťte prosím předchozí kód (výše, dodržuje tento prvek) a poté kód níže. Uvidíte, jak MutationObserver rozpozná a zvýrazní úryvek.

Ukázkový prvek s id="highlight-demo" , spusťte výše uvedený kód a sledujte jej.

Následující kód vyplní jeho innerHTML , což způsobí MutationObserver reagovat a zvýraznit jeho obsah:

let demoElem = document.getElementById('highlight-demo');

// dynamically insert content with code snippets
demoElem.innerHTML = `A code snippet is below:
 <pre class="language-javascript"><code> let hello = "world!"; </code></pre>
 <div>Another one:</div>
 <div>
 <pre class="language-css"><code>.class { margin: 5px; } </code></pre>
 </div>
`;

Nyní máme MutationObserver který může sledovat veškeré zvýraznění v pozorovaných prvcích nebo celý document . Můžeme přidávat/odebírat úryvky kódu v HTML, aniž bychom o tom přemýšleli.

Další metody

Existuje způsob, jak zastavit pozorování uzlu:

  • observer.disconnect() – zastaví pozorování.

Když zastavíme pozorování, je možné, že některé změny ještě nebyly pozorovatelem zpracovány. V takových případech používáme

  • observer.takeRecords() – získá seznam nezpracovaných záznamů mutací – těch, které se staly, ale zpětné volání je nezpracovalo.

Tyto metody lze použít společně, například takto:

// get a list of unprocessed mutations
// should be called before disconnecting,
// if you care about possibly unhandled recent mutations
let mutationRecords = observer.takeRecords();

// stop tracking changes
observer.disconnect();
...
Záznamy vrácené observer.takeRecords() jsou odstraněny z fronty zpracování

Zpětné volání nebude voláno pro záznamy, které vrátí observer.takeRecords() .

Interakce sběru odpadu

Pozorovatelé interně používají slabé odkazy na uzly. To znamená, že pokud je uzel odstraněn z DOM a stane se nedosažitelným, může být sesbírán.

Pouhá skutečnost, že je pozorován uzel DOM, nezabrání shromažďování odpadu.

Shrnutí

MutationObserver dokáže reagovat na změny v DOM – atributy, obsah textu a přidávání/odebírání prvků.

Můžeme jej použít ke sledování změn zavedených jinými částmi našeho kódu a také k integraci se skripty třetích stran.

MutationObserver může sledovat jakékoli změny. Možnosti konfigurace „co pozorovat“ se používají k optimalizaci, nikoli k utrácení prostředků na nepotřebná volání zpětného volání.