Getting to know mutation observers
As you develop more complex JavaScript-heavy applications or roll-your-own framework, you may find that you need to know when the DOM node tree has changed. You may want to know when a view has been loaded or unloaded. Or perhaps you are profiling an application, and want to measure how many nodes are affected by a DOM operation.
We used to do this with mutation events. Introduced by the DOM, Level 2 specification, the MutationEvent interface defined several events — such as DOMNodeInserted
and DOMAttrModified
— that would be fired by the browser when a node was added, removed, or deleted. Mutation events, however, are not without their problems.
The problem with MutationEvents
Though an excellent idea in theory, in practice, mutation events had two major hurdles.
- MutationEvents are synchronous. Events are fired when called, and may prevent other events in the queue from being fired. Add or remove enough nodes, and the application could lag or hang.
- Because they were events, they were implemented as events. I know that reads like circular logic, but stick with me. Events must propagate through the DOM via capturing and sometimes bubbling. Capturing and bubbling can, in turn, trigger other event listeners that modify the DOM. And those can, in turn, cause more MutationEvents to fire, clogging the JavaScript thread — or worse, crashing the browser.
Sounds messy, right?
Indeed, mutation events are messy enough to have been deprecated in the DOM, Level 3 specification. But if mutation events are deprecated, we need something to replace them. That's where mutation observers come in.
How Mutation Observers are different
Mutation observers are defined by the DOM Standard, and differ from mutation events one key way: they are asynchronous. They do not fire every time an event occurs. Instead they:
- wait until other scripts or tasks complete;
- report changes in a batch as an array of mutation records, rather than one-by-one; and
- can observe all changes to a node, or only observe specific kinds of changes.
What's more, because they are not events, they don't come with the implementation overhead of events. They're less likely to freeze the UI or cause a browser crash as a result.
Let's consider an example. In the code below, we're appending 2500 paragraphs to a document fragment, and then adding that fragment as a child of an article element.
var docFrag = document.createDocumentFragment(), thismany = 2500, i=0, a = document.querySelector('article'), p; while ( i < thismany) { // Creates a new p element if one doesn't exists. // Clones the existing element if it does. p = (p === undefined) ? document.createElement('p') : p.cloneNode(false); docFrag.appendChild(p); i++; } a.appendChild( docFrag );
Even though we're adding 2500 paragraphs nodes, we've batched them into one DOM update by using a document fragment. Still, this bit of code generates 2500 DOMNodeInserted
events, one for each paragraph. Our DOMNodeInserted event handler is invoked 2500 times. With a mutation observer, on the other hand, our callback is invoked once. One mutation observer can record multiple DOM operations.
Okay, but can I use them now?
Support for isn't available everywhere just yet. Opera 15+, Firefox 14+ and Chrome 26+ support the MutationObserver
interface. Internet Explorer 11 will also have support when it's released, as will Safari 6.1. Safari 6.0 and Chrome versions 18 through 25 also support MutationObserver
, but with a WebKit prefix (WebKitMutationObserver
). You can detect support with the code shown below.
var canObserveMutation = 'MutationObserver' in window;
So how do I use 'MutationObserver'?
The good news is that mutation observers are easy to use. First create an observer object using the MutationObserver constructor as shown in Figure 3. The constructor requires a single parameter, a callback function.
var observer, callback; callback = function( recordqueue ){ // do something to each record in the recordqueue array. } observer = new MutationObserver( callback );
Our callback function will receive an array of MutationRecord
objects as an argument. Each MutationRecord
object summarizes a change to the node tree. We'll discuss mutation records in more detail later.
Next, you'll need to define a node to observe, and determine what kinds of DOM changes you'd like to keep an eye on. For this, we use the observe
method. Its first parameter must be a node, and its second must be a dictionary of options (Figure 4). In the example below, we'll watch an article element for changes to its children or attributes.
var options = { 'childList': true, 'attributes':true }, article = document.querySelector( 'article' ); observer.observe( article, options );
The options parameter may include the following properties and values.
childList
- true or false; observe mutations to the target node's children.
attributes
- true or false; observe changes to the attributes of a target node.
characterData
- true or false; Observe changes to the data or text content of the target node.
subtree
- true or false; observe mutations to all descendants of the target, including child nodes and "grandchild nodes" (or the child nodes of child nodes).
attributeOldValue
- true or false; if the attributes property is true, and you'd like to capture the value of the attribute before the mutation is recorded.
characterDataOldValue
- true or false; if the characterData property is true, and you'd like to capture the value of the data before the mutation is recorded.
attributeFilter
- a list of attributes to observe, enclosed in square brackets (example:
['class','src']
);
Either the childList
, attributes
, or characterData
property must be included, and set to true
in order to observe a mutation.
To stop observing mutations, use the disconnect()
method (observer.disconnect()
). Using this method prevents further invocation of the callback function. The takeRecord
method (observer.takeRecord()
) clears the record queue. To resume watching mutations, just re-invoke the observe
method.
I mentioned above that the mutation callback receives an array of mutation records as an argument. Let's take a look at what a mutation record is.
Mutation records
A mutation record is an object that reports a single change to the document tree. Mutation record objects are defined by the MutationRecord
interface, and contain the following items.
type
- the type of of mutation observed, either
attribute
,characterData
orchildList
. target
- the node affected by the mutation.
addedNodes
- a NodeList of elements, attributes, and text nodes added to the tree.
removedNodes
- a NodeList of elements, attributes, and text nodes removed from the tree.
previousSibling
- returns the previous sibling node, or null if there is no previous sibling.
nextSibling
- returns the next sibling node, or null if there is no next sibling.
attributeName
- The name of the attribute or attributes changed. If
attributeFilter
option was set, it will only return the filtered node. oldValue
- the pre-mutation value in the case of attribute or
characterData
mutations, andnull
forchildList
mutations.
Now that we've covered the syntax of mutation observers and mutation records, let's look at some examples.
Observing the addition or removal of child nodes
Observing the addition or removal of child nodes is pretty straightforward. We'll create a new object and pass a callback. We'll also observe our document's body, and all changes to its children. Figure 5 shows how.
var callback = function(allmutations){ // Since allmutations is an array, we can use JavaScript Array methods. allmutations.map( functions(mr){ var mt = 'Mutation type: ' + mr.type; // log the type of mutation mt += 'Mutation target: ' + mr.target; // log the node affected. console.log( mt ); }); }, mo = new MutationObserver(callback), options = { // required, and observes additions or deletion of child nodes. 'childList': true, // observes the addition or deletion of "grandchild" nodes. 'subtree': true } mo.observe(document.body, options);
Notice that we've included the subtree
option, and set it to true
. Doing so captures when children are appended to the document body (example: document.body.appendChild(el)
), and when they are appended to a child of the body (document.getElementById('my_element').appendChild(el)
). If, instead, subtree
was false
or missing, the observer would only keep track of elements appended to the body.
It's also possible to observe mutations to document fragments. Just pass the fragment as the first parameter to the observe
method.
Observing changes to attributes
Observing changes to attributes works much the same way. The main difference is that you must add 'attributes': true
to the options dictionary. If you also want to record the previous attribute value, set the attributeOldValue
option to true
(view a demo).
var callback = function(allmutations){ // Since allmutations is an array, we can use array functions. allmutations.map( functions(mr){ // log the previous value of the attribute. var attr = 'Previous attribute value: ' + mr.oldValue; console.log(attr); }); }, element = document.getElementById('my_el'), mo = new MutationObserver(callback), options = { 'attributes': true, // required 'attributeOldValue': true // captures the previous attribute value. } mo.observe(element, options);
The example above will capture all changes to any attribute of our target element, including deletions. As you can see in the demo, each time the value of an attribute changes, a new mutation record gets added to the queue. But what if we only wanted to observe changes to particular attributes?
Filtering which attributes are observed
We can limit the which attributes we'd like to observe by adding the attributeFilter
property to our options (Figure 7). The value of attributeFilter
must be a comma-separated list of attributes to track, enclosed in square brackets ([
and ]
).
var options = { 'attributes': true, 'attributeOldValue': true, 'attributeFilter': ['class'] // only captures changes to the class attribute } mo.observe(element, options);
Setting that property means that a mutation record wil be generated only for changes to the value of the class attribute (view a demo).
Learn More
To learn more about mutation observers, try the following resources.
- Mutation observers from the WHATWG
- Mutation Observers vs Mutation Events from the Mutation Summary project.
- MutationReplacement, from the W3C WebApps wiki, which offers historical and technical context
Cover photo by gerlos. Used under a Attribution-ShareAlike 2.0 Generic license.
This article is licensed under a Creative Commons Attribution 3.0 Unported license.
Comments