Modifications

Invoking Modification Listeners

There are two candidate solutions for when the modification listener is to be invoked:

Directly after the modification.

At the end of the current task.

For a piece of JavaScript code such as (which is run as one task for the purposes of this example):

node.appendChild(newChild)
node.appendChild(newChild)
somethingelse()

... approach 1 means the listener is dispatched twice just before the appendChild() method returns and approach 2 means the listener is dispatched after somethingelse() has completed.

PRO 1: It works almost identical to mutation events, without the issue the latter presents to UA implementors.

PRO 2: More changes are batched.

Mutation listeners only have to operate on the final state rather than each intermediary state.

CON 1: If the script running on the page and the script handling the modification listeners are highly decoupled they might present problems for each other similar to how mutation events present issues to UA implementors.

CON 2: Different model from how mutation events work today.

If a task groups modifications and showModalDialog() (what about <dialog modal>?) this creates trouble.

Old

Options:

Immediately: i.e. while the operation is underway. [Note: This is how current DOM Mutation events work].

After modifications: Upon completion of the "outer-most" DOM operation. i.e. Immediately before a the lowest-on-the-stack DOM operation returns, but after it has done all of its work.

End of current task: i.e. immediately before the UA is about to fetch a new Task to run.

New task: i.e. fully async.

Immediately

After modifications

End of current task

New task

Pro

It's conceptually the easiest thing to understand. The following *always* hold:

For calling code: When any DOM operation I make completes, all observers will have run.

For notified code: If I'm being called, the operation which caused this is below me on the stack.

It's conceptually close to fully synchronous. For simple uses (specifically, setting aside the case of making DOM operations within a mutation callback), it has the advantages of Option 1, without its disadvantages. Because of this, it's similar to the behavior of current Mutation Events.

Semantics are consistent: delivery happens right before the outermost DOM operation returns.

Easier transition from mutation events to the new API.

Not bound to tasks. Side effects, like problems related to spinning event loop are per mutation callback, not per whole task.

No code is at risk for having its assumptions invalidated while it is trying to do work. All participants (main application script, libraries which are implemented using DOM mutation observation) are allowed to complete whatever work (DOM operations) they wish before another participant starts doing work.

Can batch more, since the callbacks are called later than in option 2.

Creates (and requires use of -- creates a "pit of success") a time to run which is ideal in two ways:

Performance: The main script is finished doing its work. The observer can minimize work by reacting to only the net-effect of what happened. I.e. not do work in intermediate states which ultimately become irrelevant. E.g. a widget library which needs to "destruct" widgets which are removed from the document. If a widget is removed but later added elsewhere in the same script event, the library would prefer to avoid destructing the widget and just allow it to be moved.

Correctness: The observer isn't at risk for attempting to act when the main script has put the DOM (temporarily) in an inconsistent state. E.g. a templating library which depends upon the value of two attributes of a single element. If script wishes to change both values but cannot do so without creating a temporarily nonsensical state, the library would prefer not to have to react to the nonsensical state and simply wait for the consistent (final) state.

Conceptually easy to understand

Easy to implement.

Con

Because mutations must be delivered for some DOM operations before the operation is complete, UAs must tolerate all ways in which script may invalidate their assumptions before they do further work.

The timing delays delivery just long enough to guarantee that DOM operations don't have to worry about having their work interfered with, but encourages application script to leave itself exposed to exactly the same risk.

The semantics of delivery are inconsistent. Delivery of mutations is synchronous if calling operation is performed outside of a mutation callback and async if performed inside a mutation callback.

Behaves quite differently from the current mutation events.

Since the approach is bound to tasks, it is not clear what should happen if event loop spins while handling the task. What if some other task modifies the DOM, when should the mutation callbacks fire? Because of this issue, tasks, which may spin event loop, should not also modify DOM since that may cause some unexpected result.

Callback handling is moved far away from the actual mutation.

It's too late. Most use cases for mutation observation require that observers run before a paint occurs. E.g. a widget library which
watches for special attributes. Script may create a

and an observer will react to this by decorating the div as a FooButton. It is unacceptable (creates visual artifacts/flickering) to have the div be painted before the widget library has decorated it as a FooButton.

Both of these options appear to be non-starters. Option 1 has been shown by experience to be an unreasonable implementation burden for UAs. Option 4 clearly doesn't handle properly important use cases.