Re: Mutation Observers: a replacement for DOM Mutation Events

On Mon, Sep 26, 2011 at 11:05 AM, Olli Pettay <Olli.Pettay@helsinki.fi> wrote:
> 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();
>> };
>>
>> 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.
>
> We need to change this name, since per WebIDL 'attribute' can't be used
> here.
>
> I propose we use attr everywhere in the API. MutationRecord has already
> attrName.
> attributeOldValue -> attrOldValue and attributeFilter -> attrFilter

Good catch (and I should've caught it when typing this up, as it's
pseudo-idl), 'attr' works for me.  If that shortening isn't
appreciated, we could go with 'attributes' instead.

- Adam

>
>
>>     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 Monday, 26 September 2011 18:09:42 UTC