This commit is contained in:
Ilya Kantor 2019-08-06 12:48:27 +03:00
parent b7a0591007
commit 68fdeeef3f

View file

@ -25,12 +25,14 @@ observer.observe(node, config);
- `childList` -- changes in the direct children of `node`,
- `subtree` -- in all descendants of `node`,
- `attributes` -- attributes of `node`,
- `attributeOldValue` -- record the old value of attribute (infers `attributes`),
- `characterData` -- whether to observe `node.data` (text content),
- `characterDataOldValue` -- record the old value of `node.data` (infers `characterData`),
- `attributeFilter` -- an array of attribute names, to observe only selected ones.
- `characterData` -- whether to observe `node.data` (text content),
Then after any changes, the `callback` is executed, with a list of [MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects as the first argument, and the observer itself as the second argument.
Few other options:
- `attributeOldValue` -- if `true`, pass both the old and the new value of attribute to callback (see below), otherwise only the new one (needs `attributes` option),
- `characterDataOldValue` -- if `true`, pass both the old and the new value of `node.data` to callback (see below), otherwise only the new one (needs `characterData` option).
Then after any changes, the `callback` is executed: changes are passed in the first argument as a list of [MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects, and the observer itself as the second argument.
[MutationRecord](https://dom.spec.whatwg.org/#mutationrecord) objects have properties:
@ -38,12 +40,11 @@ Then after any changes, the `callback` is executed, with a list of [MutationReco
- `"attributes"`: attribute modified
- `"characterData"`: data modified, used for text nodes,
- `"childList"`: child elements added/removed,
- `target` -- where the change occurred: an element for "attributes", or text node for "characterData", or an element for a "childList" mutation,
- `target` -- where the change occurred: an element for `"attributes"`, or text node for `"characterData"`, or an element for a `"childList"` mutation,
- `addedNodes/removedNodes` -- nodes that were added/removed,
- `previousSibling/nextSibling` -- the previous and next sibling to added/removed nodes,
- `attributeName/attributeNamespace` -- the name/namespace (for XML) of the changed attribute,
- `oldValue` -- the previous value, only for attribute or text changes.
- `oldValue` -- the previous value, only for attribute or text changes, if the corresponding option is set `attributeOldValue`/`characterDataOldValue`.
For example, here's a `<div>` with `contentEditable` attribute. That attribute allows us to focus on it and edit.
@ -54,16 +55,17 @@ For example, here's a `<div>` with `contentEditable` attribute. That attribute a
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});
// observe everything except attributes
observer.observe(elem, {
// observe everything except attributes
childList: true,
subtree: true,
characterDataOldValue: true
childList: true, // observe direct children
subtree: true, // and lower descendants too
characterDataOldValue: true // pass old data to callback
});
</script>
```
If we change the text inside `<b>edit</b>`, we'll get a single mutation:
Now if we change the text inside `<b>edit</b>`, we'll get a single mutation:
```js
mutationRecords = [{
@ -87,22 +89,36 @@ mutationRecords = [{
}, {
type: "characterData"
target: <text node>
// ...details depend on how the browser handles the change
// ...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 can just delete the extra space after "edit".
// may be one mutation or a few
// or it may leave them separate text nodes
}];
```
## Observer use case
So, `MutationObserver` allows to react on any changes within DOM subtree.
When `MutationObserver` is needed? Is there a scenario when such thing can be useful?
## Usage for integration
We can track something like `contentEditable` and implement "undo/redo" functionality (record mutations and rollback/redo them on demand). There are also cases when `MutationObserver` is good from architectural standpoint.
When such thing may be useful?
Imagine the situation when you attach a third-party script that adds useful functionality on the page, but also does something unwanted, e.g. shows ads `<div class="ads">Unwanted ads</div>`.
Naturally, the third-party script provides no mechanisms to remove it.
Using `MutationObserver`, we can detect when such element appears in our DOM and remove it. While leaving the useful functionality intact. Surely though, creators of that script won't be happy that you took their useful stuff and removed the ads.
There are other situations when a third-party script adds something into our document, and we'd like to detect, when it happens, to adapt our page, dynamically resize something etc.
`MutationObserver` can easily handle this.
## Usage for architecture
There are also situations when `MutationObserver` is good from architectural standpoint.
Let's say we're making a website about programming. Naturally, articles and other materials may contain source code snippets.
An HTML markup of a code snippet looks like this:
Such snippet in HTML markup looks like this:
```html
...
<pre class="language-javascript"><code>
@ -114,20 +130,13 @@ An HTML markup of a code snippet looks like this:
Also we'll use a JavaScript highlighting library on our site, e.g. [Prism.js](https://prismjs.com/). A call to `Prism.highlightElem(pre)` examines the contents of such `pre` elements and adds into them special tags and styles for colored syntax highlighting, similar to what you see in examples here, at this page.
When to run that method? We can do it on `DOMContentLoaded` event, or at the bottom of the page. At that moment we have DOM ready, can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them:
When exactly to run that highlighting method? We can do it on `DOMContentLoaded` event, or at the bottom of the page. At that moment we have DOM ready, can search for elements `pre[class*="language"]` and call `Prism.highlightElem` on them:
```js
// highlight all code snippets on the page
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);
```
Now the `<pre>` snippet looks like this (without line numbers by default):
```js
// here's the code
let hello = "world";
```
Everything's simple so far, right? There are `<pre>` code snippets in HTML, we highlight them.
Now let's go on. Let's say we're going to dynamically fetch materials from a server. We'll study methods for that [later in the tutorial](info:fetch). For now it only matters that we fetch an HTML article from a webserver and display it on demand:
@ -159,11 +168,11 @@ And what if the content is loaded by a third-party module? E.g. we have a forum
Luckily, there's another option.
We can use `MutationObserver` to automatically detect code snippets inserted in the page and highlight them.
We can use `MutationObserver` to automatically detect when code snippets are inserted in the page and highlight them.
So we'll handle the highlighting functionality in one place, relieving us from the need to integrate it.
## Dynamic highlight demo
### Dynamic highlight demo
Here's the working example.
@ -173,7 +182,7 @@ If you run this code, it starts observing the element below and highlighting any
let observer = new MutationObserver(mutations => {
for(let mutation of mutations) {
// examine new nodes
// 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)
@ -184,7 +193,7 @@ let observer = new MutationObserver(mutations => {
Prism.highlightElement(node);
}
// maybe there's a code snippet somewhere in its subtree?
// or maybe there's a code snippet somewhere in its subtree?
for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
Prism.highlightElement(elem);
}
@ -198,7 +207,11 @@ let demoElem = document.getElementById('highlight-demo');
observer.observe(demoElem, {childList: true, subtree: true});
```
<p id="highlight-demo" style="border: 1px solid #ddd">Demo element with <code>id="highlight-demo"</code>, obverved by the example above.</p>
Here's HTML-element and JavaScript that dynamically fills it using `innerHTML`.
Please run the previous code (above, observes that element), and then the code below. You'll see how `MutationObserver` detects and highlights the snippet.
<p id="highlight-demo" style="border: 1px solid #ddd">Демо-элемент с <code>id="highlight-demo"</code>, за которым следит код примера выше.</p>
The code below populates `innerHTML`. Please run the code above first, it will watch and highlight the new content:
@ -223,18 +236,17 @@ There's a method to stop observing the node:
- `observer.disconnect()` -- stops the observation.
Additionally:
Another method often used with it:
- `mutationRecords = observer.takeRecords()` -- gets a list of unprocessed mutation records, those that happened, but the callback did not handle them.
```js
// we're going to disconnect the observer
// we'd like to stop tracking changes
observer.disconnect();
// it might have not yet handled some mutations
let mutationRecords = observer.takeRecords();
// process mutationRecords
// now all handled, disconnect
observer.disconnect();
```
## Garbage collection
@ -245,6 +257,6 @@ Observers use weak references to nodes internally. That is: if a node is removed
`MutationObserver` can react on changes in DOM: attributes, added/removed elements, text content.
We can use it to track changes introduced by other parts of our own or 3rd-party code.
We can use it to track changes introduced by other parts of our code, as well as to integrate with third-party scripts.
For example, to post-process dynamically inserted content, as demo `innerHTML`, like highlighting in the example above.
`MutationObserver` can track any changes. The config "what to observe" options are used for optimizations, not to spend resources on unneeded callback invocations.