Mutation Observers: a replacement for DOM Mutation Events

Chromium (myself, Rafael Weinstein, Erik Arvidsson, Ryosuke Niwa) and
Mozilla (Olli Pettay, Jonas Sicking) have worked together on a
proposal for a replacement for Mutation Events.

This proposal represents our best attempt to date at making a set of
sensible trade offs which allows for a new mutation observation
mechanism that:

- Is free of the faults of the existing Mutation Events mechanism
(enumerated in detail here:
http://lists.w3.org/Archives/Public/public-webapps/2011JulSep/0779.html)

- Meets the goal of having the main “questions” that use-cases will
need answered about the net-effect of changes, be computable in linear
time complexity roughly proportional to the number of changes that
occurred.

Significant aspects of this design:

- Delivery of MutationRecords happens asynchronously, at the end of
the current “microtask”. This is between Options 2 and 3 from this
discussion http://lists.w3.org/Archives/Public/public-webapps/2011JulSep/0780.html.
Instead of calling listeners at the end of outermost DOM operation or
at the end of a Task, listeners are called at the end of outermost
script invocation. If there are no script invocations, listeners are
called at the end of Task.

- Information about mutations is delivered to observers as an ordered
sequence of MutationRecords, representing an observed sequence of
changes that have occurred.

- Subtree observation properly handles the case where nodes are
transiently removed from, mutated outside of and then returned to the
subtree.

- Observers specify the types of changes they are interested in and
(in some cases) level of detail they require.

Sample usage:

var observer = new MutationObserver(function(mutationRecords) {
  // Handle mutations
});

observer.observe(myNode,
{  // options:
  subtree: true;  // observe the subtree rooted at myNode
  childList: true;  // include information childNode insertion/removals
  attribute: true;  // include information about changes to attributes
within the subtree
});

…

observer.disconnect();  // Cease observation

Details:

We introduce a new interface MutationObserver with a constructor on DOMWindow:

[Constructor(in MutationCallback callback)]
interface MutationObserver {
   void observe(in Node target, in MutationObserverOptions options);
   void disconnect();
};

where MutationCallback is

[Callback, NoInterfaceObject]
interface MutationCallback {
    void handleEvent(in MutationRecord[] mutations, in
MutationObserver observer);
};

Registration & Observation
- A call to observe creates a registration for the observer to be
delivered mutations made to |target|, and optionally, its descendants.

- Subsequent calls to the same MutationObserver made with the same
|target| have the effect of resetting the options associated with the
registration.

- Subsequent calls to the same MutationObserver made with different
|targets| have the effect of expanding the set of nodes which are
being observed by the observer. All mutations made to all observed
nodes in all registrations for a given observer are delivered, in
time-ordered sequence, via a single invocation of the
MutationCallback’s handleEvent method.

- disconnect ceases observation over the observer’s set of observed nodes.

Registration Options
The |options| argument provided in observe is defined by the
MutationObserverOptions interface:

interface MutationObserverOptions {
    // Mutation types
    boolean childList;  // If true, mutations affecting node’s
childNodes are included.
    boolean attribute;  // If true, mutations affecting element’s
attributes are included.
    boolean characterData;  // If true, mutations affecting the value
of CharacterData
                                            //nodes are included.
    // [Note: If none of the known mutation types is specified, an
Error is thrown]

    // Subtree observation
    boolean subtree;  // If true, the observed set of nodes for this
registration should include
                                 // descendants of MutationTarget
(behavior described below).

    // Old values
    boolean attributeOldValue;
    // If true, MutationRecords describing changes to attributes should
    // contain the value of the attribute before the change. If true
    // without attribute: true specified, an Error is thrown.

    boolean characterDataOldValue;
    // If true, MutationRecords describing changes to
    // CharacterData nodes should contain the value
    // of the node before the change. If true without
    // characterData: true, an Error is thrown.

    // Filtering
    DOMString[] attributeFilter;
    // If provided, only changes to attributes with localName equaling
    // one of the provided strings will be delivered. If provided without
    // attribute: true, an Error is thrown.
};

Subtree Observation
If the subtree option is requested during registration, the observer
is delivered mutations which occur to a set of observed nodes which is
computed as follows:

- At the time of registration, the set includes |target| and all
descendant nodes
- At any point, if a node becomes a descendant of |target|, it is
synchronously added to the observed set
- Immediately before delivering all pending MutationRecords to the
observer, all nodes which are no longer descendants of |target| are
removed from the observed set.

Processing Model:

Pending Mutation Queues
For each MutationObserver with at least one active registration, the
UA maintains a queue of pending MutationRecords which are pending
delivery.

Record Creation & Enqueuing
MutationRecords are created at the following times:

-When one or more nodes are added to and/or removed from a node’s
childNodes collection, a 'childList' record is created with target set
to that node.

Each childList MutationRecord represents that, at a given position in
the childNodes collection, a contiguous sequence of nodes was removed
and a contiguous sequence of nodes was inserted. In this way, the
effect on the target node of many (but not all, e.g. execCommand) DOM
operations may be represented by a single child list change record
(e.g. innerHTML, insertBefore, appendChild). The position at which the
nodes were inserted and/or removed is recorded via the previousSibling
and nextSibling attributes of MutationRecord.

- When an attribute is added, removed or changed, an 'attribute'
record is created with target set to the element owning the attribute.

- When the data attribute of a CharacterData (Text or Comment node) is
changed a 'characterData' record is created with the target set to
that CharacterData node.

For each observer, if a registration exists which requests the
matching mutation type and whose observed node set contains the target
node of the mutation, a MutationRecord is appended to the observer's
pending mutation queue. If multiple such registrations exist for a
given observer, a single MutationRecord is delivered having the union
of the information requested by all registrations (e.g.
attributeOldValue).

Delivery of Pending Mutations
If mutations occur during script invocation, delivery must take place
at the end of the current “microtask”. If mutations are made directly
by the UA and not by script (for example due to user input), delivery
must take place by the end of the current Task.

The steps for delivery are:

1) Visit each observer, in registration order

For each observer,

2) Remove any non-descendants from the observed set of any subtree
observation associated with this observer

3) If the observer’s pending mutation queue is non-empty, clear the
queue, putting the contents into a MutationRecord[] array and call the
observer’s handleEvent method, passing the newly created array of
records.  If the observer’s associated callback is a bare function
(and not an object with a handleEvent method), it is called with
|this| set to the observer.

4) If any observer’s pending queue has become non-empty, goto step 1.

Mutation Records

interface MutationRecord {
   // Mutation type: one of 'childList', 'attribute', or 'characterData'
   readonly attribute DOMString type;

   // For childList and attributes, target is the owner node affected.
   // For CharacterData, target is the node affected.
   readonly attribute Node target;

   // For type == 'childList’, Sequence of added and removed nodes in
this operation.
   readonly attribute NodeList addedNodes;
   readonly attribute NodeList removedNodes;

   // For type == 'childList’, The siblings in childNodes immediately
preceding following the first
   // and last nodes added and/or removed.
   readonly attribute Node previousSibling;
   readonly attribute Node nextSibling;

   // For type == 'attribute', the name and namespaceURI of the
attribute affected
   readonly attribute DOMString attrName;
   readonly attribute DOMString namespaceURI;

   // For type == ‘attribute’ or ‘characterData’, if requested, the
value immediately
   // preceding the mutation.
   readonly attribute DOMString oldValue;
};

MutationRecords are immutable. All the WebIDL attributes are
non-writable and non-configurable. The UA may deliver the same
MutationRecord to multiple observers just like Events, MutationRecords
are still extensible so they could be used as side channels for
communication.

However, observers must only be delivered MutationRecords with the
exact level of detail that they requested (e.g. If an observer
requested attribute mutations, but not attributeOldValue it should
never be delivered an ‘attribute’ type MutationRecord’s containing
oldValue).

My next plans are to begin a vendor-prefixed implementation of this
API in WebKit and to begin work on formalizing the above into a proper
spec.

- Adam

Received on Friday, 23 September 2011 21:17:16 UTC