MutationObserver callback not called if disconnect called immediately after modifying the DOM

StackOverflow https://stackoverflow.com/questions/22096340

  •  18-10-2022
  •  | 
  •  

Question

I'm building an undomanager, similar to the W3C undomanager that's not quite yet ready in the various browsers. I implemented a simply transact call that calls a callback while watching for changes to the DOM, and then adds the necessary structures to an array that can later be used to undo (or redo) the change.

A simple example:

function transact(callback){
    /* Watch content area for mutations */
    observer = new MutationObserver(function(){
        /* TODO: collect mutations in here */
        alert('Mutations observed');
    });
    observer.observe(document.getElementById('content'), {
      attributes: false,
      childList: true,
      characterData: false,
      subtree: false
    });

    /* Perform the callback */
    callback();

    /* Stop observing */
    //observer.disconnect();
    setTimeout(function(){ observer.disconnect();}, 1);

}

To use this:

transact(function(){
    var p = document.createElement('p');
    p.innerHTML = 'Hello';
    document.getElementById('content').appendChild(p);
});

If I call observer.disconnect() immediately, the mutation observer never reaches the alert call, but if I use setTimeout, it works fine.

I would be perfectly happy to live with the setTimeout call, the only problem seems to be that for larger changes, you have to delay the disconnect as much as 800 milliseconds.

It is almost as if the disconnect happens before the DOM change has actually been completed, and so nothing is detected.

This happens in both Firefox 25 and Chrome 32.

I thought for a second that because observer is a local variable, perhaps it goes out of scope too soon, but changing it to a global variable didn't help. I have to delay the call to disconnect() to give the DOM a chance to catch up it seems.

Is this a browser bug? Is there a better way to call disconnect() as soon as the DOM is ready again?

Was it helpful?

Solution

MutationObservers are async by specfication, in that they will wait for the current stack to be empty before it calls your callback function. This is useful so your callback is not called each time you make a change to the DOM but only after all your changes have been made. See how are MutationObserver callbacks fired?

If you look at the specification link you will notice the steps involved before a MutationEvent are:

  • MutationObserver gets notified of a mutation
  • Appends the mutation to the current set of mutations since the last event/takeRecords
  • Call the callback function after the current stack is empty (this is why your code works as you expected with set timeout - setTimeout will call the function after the timeout and stack empties)
  • Empty the record queue and continue observing

Update sorry, to address your actual question, I'm thinking it may have to do with the alert in the MutationObserver callback. It definitely shouldn't take more than a couple of milliseconds for mutations to be processed and it should definitely occur before the setTimeout. Anyway a solution that would definitely work is to add a queue processor in the MutationObserver callback instead of using a timeout.

function transact(callback){
    var queue = [], listener; //queue of callbacks to process whenever a MO event occurs
    /* Watch content area for mutations */
    var observer = new MutationObserver(function(){ //made observer local
        /* TODO: collect mutations in here */
        alert('Mutations observed');
        while(listener = queue.shift()) listener();
    });
    observer.observe(document.getElementById('content'), {
      attributes: false,
      childList: true,
      characterData: false,
      subtree: false
    });

    /* Perform the callback */
    callback();

    /* Stop observing */
    //observer.disconnect();
    queue.push(observer.disconnect.bind(observer));

}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top