Search code examples
javascriptdommutation-observers

Why does MutationObserver fire twice for childList but never for characterData?


I have a simple MutationObserver setup as a test. The HTML has a span whose text content gets updated once per second (and a div for messages):

<span class="tester"></span>
<div id="msg"></div>

The MutationObserver is set to watch .tester and writes text to the #msg div when it observes a change. Meanwhile, a setInterval() runs once/second to change the text in .tester:

var target = document.querySelector('.tester');

var observer = new MutationObserver(function(mutations) {
  mutations.forEach(function(mutation) {
    console.log(mutation.type);
    $('#msg').append(mutation.type+"<br/>")
    setTimeout(function() { $('#msg').text(''); }, 500);
  });    
});

var config = { attributes: true, childList: true, characterData: true };
observer.observe(target, config);

setInterval(function() {
  $('#msg').text('');
  $('.tester').text("update: "+Math.random())
}, 1000);

I would expect this code to print once per second that the characterData has changed. According to Mozilla's docs for MutationObserver, it says about characterData: "Set to true if mutations to target's data are to be observed." Instead, I see no characterData mutations but do see two childList mutations every second.

Why am I not seeing any characterData mutations, and why am I seeing two childList mutations?

Here's a working example with CodePen.


Solution

  • The reason is as Jeremy Banks said: When you use jQuery's text(), it removes all the text nodes and then adds in new ones. That's not a change to character data, it's a change to the childList: Removing the node that's there and replacing it with a new one.

    To see a change to character data, you have to modify the existing text node's nodeValue, and observe subtree modifications:

    var target = document.querySelector('.tester');
    
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            console.log(mutation.type);
            $('#msg').append(mutation.type+"<br/>")
            setTimeout(function() { $('#msg').text(''); }, 500);
        });    
    });
    
    var config = {
        attributes: true,
        childList: true,
        characterData: true,
        subtree: true                   // <=== Change, added subtree
    };
    observer.observe(target, config);
    
    var timer = setInterval(function() {
        $('#msg').text('');
        // Change --VVVVV modifying the existing child node
        $('.tester')[0].firstChild.nodeValue = "updated" + Math.random();
    }, 1000);
    
    // Stop after 10 seconds
    setTimeout(function() {
        clearInterval(timer);
    }, 10000);
    <span class="tester">x</span><!-- Change, added a starting child node -->
    <div id="msg"></div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>


    Re your question about why there are two childList mutations, yes I think you're right: They're removing the child, then adding a new one. If we use the replaceChild method, we see only a single mutation:

    var target = document.querySelector('.tester');
    
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            console.log(mutation.type);
            $('#msg').append(mutation.type+"<br/>")
            setTimeout(function() { $('#msg').text(''); }, 500);
        });    
    });
    
    var config = {
        attributes: true,
        childList: true,
        characterData: true,
        subtree: true                   // <=== Change, added subtree
    };
    observer.observe(target, config);
    
    var timer = setInterval(function() {
        $('#msg').text('');
        // Change --VVVVV modifying the existing child node
        var text = document.createTextNode("updated" + Math.random());
        var parent = $('.tester')[0];
        parent.replaceChild(text, parent.firstChild);
    }, 1000);
    
    // Stop after 10 seconds
    setTimeout(function() {
        clearInterval(timer);
    }, 10000);
    <span class="tester">x</span><!-- Change, added a starting child node -->
    <div id="msg"></div>
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>