Evolving a New Mutation

I used to be a big fan of DOM Mutation Events. They provided a unique way for scripts to monitor changes in the DOM, irrespective of the event or action that caused them. So events like DOMNodeInserted and DOMAttrModified would fire in response to the addition of nodes, or to attribute changes (respectively).

But if you’ve never used mutation events, that’s not really surprising, since much of the time it’s you that adds those nodes, or changes those attributes, and why would you need a reactive event for something you caused in the first place?

So they were mostly used for problem-solving in libraries and frameworks, for example, to respond to changes that originate from anonymous closures. They were also quite a stock-in-trade for many browser extensions, where they provided the simplest, and sometimes the only way, to detect when the document changes.

The syntax was very simple, just like any other event:

element.addEventListener('DOMNodeInserted', function(e)
{
  console.log('Added ' + e.target.nodeName
    + ' to ' + element.nodeName);

}, false);

However, that simplicity masked an underlying problem — mutation events were not well implemented, and they plagued browser development with performance and stability issues. They fire far too often, they’re slow and hard to optimize, and they’re the source of any number of potential crash bugs.

This is why mutation events been deprecated for about two years now, and Firefox Add-ons are nominally not allowed to include them at all anymore. In fact, when I released an update to Dust-Me Selectors last year, I had to ask for special permission to continue using them!

Note that DOMContentLoaded is not a mutation event, it simply has a similar kind of name. There are no such problems with that event and its use is not discouraged.

You can’t put a good idea down

Despite these problems, the idea of mutation events remained a good one, and it wasn’t long before developers at Mozilla and Google put together a new proposal, which soon became accepted as part of the DOM 4 specification.

The new API is called MutationObserver, and it is quite a bit more complicated than mutation events, but this complexity gives rise to dramatically greater control and precision.

Here’s a simple example, that responds to the addition of nodes to document.body, and writes to the console with a summary of each change:

var watcher = new MutationObserver(function(mutations)
{
  mutations.forEach(function(mutation)
  {
    for(var i = 0; i < mutation.addedNodes.length; i ++)
    {
      console.log('Added '
        + mutation.addedNodes[i].nodeName
        + ' to ' + mutation.target.nodeName);
    }
  });
});

watcher.observe(document.body, { childList : true  });

The observer callback is passed an object with data about the mutations, each member of which represents a single change. This is different from mutation events, which would fire the callback separately for each and every change!

The data contained in each mutation object depends on what’s being observed. In this case we’re only watching for changes to the target element’s children (specified by the childList parameter in the configuration object), and so the mutation object has an addedNodes property, which is a collection of references to each of added nodes.

Here’s a demo of that example, which works in Firefox 14 or later and Chrome 18 or later:

The demo has a button you can click to add a new paragraph to the page, and each time that happens, the observer will respond. Of course in practise you wouldn’t do that — you’d just use the click event to trigger whatever it is — but the point is that an observer can respond to changes caused by anything — including (and especially) scripts that you have no other control over.

I’m sure you can begin to imagine the potential for user-scripts and browser extensions, to be able to respond precisely to any changes in the DOM, whether they were caused by scripting, or by direct user interaction (for example, when the user types into a contentEditable region).

Some surprising possibilities

Now if you look at the demo in Firefox, you’ll notice that the console already shows several mutations — even before you’ve clicked the button. These occur because the observer itself isn’t wrapped in DOMContentLoaded, so it begins to work as soon as the script is executed. I discovered this by chance, simply because I prefer to script that way whenever possible, and I realised that the mutations are the browser adding nodes to the <body> — i.e. one for each of the nodes that come after the containing <script>.

Chrome doesn’t do this — and I can only suspect that it’s deliberately prevented — because it makes perfect sense in relation to how we know DOM scripting works. We know that scripts execute synchronously, and that’s why it’s possible to add to the <body> before it’s finished rendering. So if we start to observe DOM changes, we should get notification of every change that happens afterwards, even if that changed was cause by the browser’s own rendering.

This puts me in mind of an idea I had a couple of years ago, for a library that would provide callbacks for several different points during a document’s loading and rendering. I never developed that idea, because it would take such brutal hacks — but using mutation observers it would be trivial and clean. All we’d have to do is add the observer right at the start of the body, and then we could sit back and watch the browser draw it node by node!

Check it out (in Firefox 14 or later):

More every-day possibilities

In practise though, most mutation observers won’t need to be as extensive as that, and indeed, their finesse and precision is part of their beauty. The browser doesn’t have to report on every tiny change, only for us to have to filter the data to find what we want (which is tedious for us, and inefficient for the browser). With mutation observers, you only need to handle the stuff you care about, and only for as long as you need to know.

Here’s another example, that watches for changes to an element’s text (i.e. to the element’s firstChild text-node), and then stops watching as soon as a change occurs:

(new MutationObserver(function(mutations, self)
{
  mutations.forEach(function(mutation)
  {
    console.log('Changed text from &quot;'
      + mutation.oldValue + '&quot; to &quot;'
      + mutation.target.nodeValue + '&quot;');
  });

  self.disconnect();

})).observe(element.firstChild, {
      characterData : true,
      characterDataOldValue : true
      });

Notice how I’ve used a slightly different syntax there — rather than saving the instantiation to a variable, I’ve enclosed it in brackets, so we can chain the observe() command directly onto the end. Within the observer, a reference to the instance itself is passed to the callback, and we can then use that reference to disconnect.

Conclusion

This has been a broad introduction to mutation observers, that’s fairly light on the details of how they’re used; I hadn’t even mentioned the fact that Chrome’s implementation is prefixed (available for now as WebKitMutationObserver). But I wanted to focus mainly on the background to this new API, and start to get excited at the possibilities!

If there’s demand, I’ll write a follow-up article to explore them in code-heavy detail — but for now, I recommend you visit the MutationObserver documentation at MDN. There’s also another good article at the Mozilla Hacks blog.

I was pretty cheesed-off when I heard that mutation events were disappearing, because what else is there that can do the same job? Well it turns out, there is something else after all — and it’s a hundred times better!

Free book: Jump Start HTML5 Basics

Grab a free copy of one our latest ebooks! Packed with hints and tips on HTML5's most powerful new features.

No Reader comments

Comments on this post are closed.