Re: Mutation Observers: a replacement for DOM Mutation Events

On 09/24/2011 12:16 AM, Adam Klein wrote:
> 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();
> };

Yeah, these methods could return mutationobserver.
(Although I don't like the o.observe(foo).observe(bar) kind of coding
  style, since it doesn't make it clear which instance's method you're
  calling. But one doesn't need to use that.)


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

s/handleEvent/handleMutations/


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

I'm hoping to have a patch (vendor-prefixed) for this API for Gecko in
next few days.


-Olli


> and to begin work on formalizing the above into a proper
> spec.
>
> - Adam
>
>

Received on Monday, 26 September 2011 08:48:20 UTC