2

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?

izak
  • 981
  • 7
  • 10

1 Answers1

3

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));

}
Community
  • 1
  • 1
megawac
  • 10,953
  • 5
  • 40
  • 61
  • By current stack, you mean the call stack? – izak Feb 28 '14 at 14:43
  • 2
    Yeah, anything *async* in the browser can only run after the immediate call stack is empty. Tons of threads on here about asynchronous functions – megawac Feb 28 '14 at 16:30
  • @izak sorry - I didn't address the question – megawac Feb 28 '14 at 19:25
  • No problem. Thanks for that queue suggestion, I am almost certain that's going to do the trick, it seems impossible not to :-) Will report back here soon. – izak Mar 03 '14 at 09:19
  • Fwiw, I knew the callback would be called async, but I did not know that meant only once the immediate call stack was empty. The above has one small problem, but I don't think there's a way to get past it really, due to the async nature. If you make more than one change to the DOM, wrap the first in a transact() call but not the rest, then everything ends up in the transaction. This is, in other words, very useful for recording the tail-end of your scripts. Which for my application, might just be good enough. It certainly is no worse than the present setTimeout solution. – izak Mar 03 '14 at 09:52
  • @izak ya sorry about the dumb explanation - I read your post right after waking up so I missed the actual question – megawac Mar 03 '14 at 16:56