Selector-based Mutation Events

From WEBAPPS
Jump to: navigation, search

by Travis Leithead, Nov 2009, send comments to travil AT microsoft dot com

Summary

The mutation events have been criticized by both browser vendors and web authors alike (see long W3C thread ) as a necessary evil to monitor dynamic changes in the DOM. Based on current discussions in the working group, and backed by a desire to propose a more performant implementation, the following "high-performance" mutation events are proposed. This is a proposal to the W3C to adopt these APIs into a "Selectors API v2" or "DOM L4 Events" spec.


This API has two variants: a synchronous callback and an asynchronous callback.


The synchronous API incurs the same performance slowdown as standard mutation events (a requirement of the synchronous nature of the API) but has:

  1. the advantage of potentially dispatching many fewer callbacks to Javascript, which saves unnecessary execution time in web program code, and
  2. it does not require an event object to be constructed nor a propagation path to be computed.


The first advantage is enabled by allowing for a filter to be provided as part of the event registration. The filter is a simple CSS selector that allows the web developer to specify exactly what type of changes should be monitored (ignoring all other changes to the DOM).


The asynchronous API builds on the filter approach and lack of event object, but also removes the requirement to be notified synchronously of the mutation event. Instead, it "batches" DOM structural changes for a web-developer-specified time interval, and only fires the callback after the time interval has elapsed. The callback supplies a list of nodes that were either added or removed over the course of the interval.


Use cases for both a synchronous and asynchronous model have been identified by web authors; thus both are provided as part of this API.

API design considerations

The API’s naming convention and general design closely follow the Selectors API semantics, including the Nodes that implement the interface; element nodes apply the CSS selector results to the whole document and then throw out the nodes that are not proper descendants of the registered node. Document- and DocumentFragment-level registration applies to the entire document (no scoping).

NodeSelectorWatch Interface

The following interfaces define the high-performance mutation events API.

[NoInterfaceObject]
interface NodeSelectorWatch
{
   // Synchronous API ____________________________
   // Invoked immediately when the change happens 
   // (e.g., semantics of DOMNodeRemoved/DOMNodeInserted)
   void watchSelector(in DOMString selectors,
                      in SelectorElementCallback callback,
                      in boolean notifyOnMatch)
                         raises(DOMException=SYNTAX_ERR,
                                DOMException=NAMESPACE_ERR);

   // Asynchronous API ____________________________
   // Invoked after the specified minimal time has elapsed and 
   // returns the batched node list (static) of elements affected
   void watchSelectorAll(in DOMString selectors,
                         in SelectorNodeListCallback callback,
                         in boolean notifyOnMatch,
                         in unsigned long milisecondMinimumDelay)
                            raises(DOMException=SYNTAX_ERR,
                                   DOMException=NAMESPACE_ERR);

   // Overloaded to take either callback...
   void unwatchSelector(in DOMString selectors,
                        in SelectorElementCallback callback);
   void unwatchSelector(in DOMString selectors, 
                        in SelectorNodeListCallback callback);
};
Document implements NodeSelectorWatch;
Element implements NodeSelectorWatch;
DocumentFragment implements NodeSelectorWatch;
  • selectors - the CSS selector (or group of selectors) that scopes the nodes to watch
  • callback - JavaScript function to invoke when the DOM tree mutates to match (or unmatch) the provided selectors.
  • notifyOnMatch - if true, the callback is invoked when a node is added to the tree that now matches the selectors. If false, the callback is invoked when a node is changed or removed from the tree that formerly matched the selectors.
  • milisecondMinimumDelay - the time (in miliseconds) to delay before triggering the callback.

Notes:

  • selectors may contain one selector or a comma-separated list of selectors (i.e., a group)
  • Elements with a SelectorElementCallback or SelectorNodeListCallback that are moved (or removed) to documentfragments or temporary markups will continue to operate (i.e., just like querySelector behaves) until the document is navigated away.
  • watchSelector[All] can "register" multiple callbacks for the same selectors.
  • Elements that are removed from the tree are kept alive by the pending asynchronous callback.
  • [Async case] The mutations that have happened are represented in the NodeList in document order (not the order in which they occurred).
    • Implementation idea: each listener selector includes a snapshot of the node set matched at the time of registration.
    • When the minimal time is up and changes have occurred, re-run the selector, report the results of the intersection of the new node set from the original, and copy the new node set into the "original" node set for that individual callback. The next callback will contain only the differences from the last time, regardless of how many other similar (or identical) callbacks are registered.
 
[Callback=FunctionOnly]
interface SelectorElementCallback 
{
   void handleElement(in Element match,
                      in DOMString relatedSelectorText, 
                      in boolean matches);
};

[Callback=FunctionOnly]
interface SelectorNodeListCallback 
{
   void handleNodeList(in NodeList allMatches, 
                       in DOMString relatedSelectors, 
                       in boolean listMatches);
};
  • match - the matching element
  • allMatches - the nodelist (static) of matching elements. Elements in this list are guaranteed to be unique (no duplicates).
  • relatedSelectors - the css selector (or group of selectors) that now matches (or formerly matched) the element or list of elements.
  • matches - true if the element or list of elements now match the relatedSelectorText. false if the element or list of elements formerly matched the relatedSelectorText but no longer match.

Use Cases

Script libraries that need to monitor the state changes of the DOM are often initialized on the DOMContentLoaded event (or earlier inline). From that point on they need to monitor state changes to keep track of new nodes for certain tasks (e.g., to perform spell-checking, grammar checking, automatic widget-attachment/ detachment/ or state change updates based on DOM attribute or element presence or removal).


In these scenarios, the current state of the DOM can easily be queried using the existing Selectors API. After the initial load has happened, these scripts simply need to watch for changes that could affect their state. These changes are often very specific to a certain element name or attribute change. Depending on the amount of tolerable delay, the synchronous or asynchronous API may be used.


The web developer creates a callback (either a SelectorElementCallback for the synchronous API, or a SelectorNodeListCallback for the asynchronous API) and then registers a listener given an appropriate set of selectors, for example:

var monitorWidget = function (widgets, selector, matches) 
{
   // TODO: for each widget in widgets
   //  if matches, then
   //   add the widget logic to the node
   //  else
   //   cleanup widget logic and reclaim
}
var widgetSig = “div > span[displaywidget]”;
// I want to be notified close-to-real-time after the 
// DOM modification happens...
document.watchSelectorAll(widgetSig, monitorWidget, true, 15);
// The cleanup can wait for a longer time...
document.watchSelectorAll(widgetSig, monitorWidget, false, 900);

Then as document mutations occur, only the ones relevant to the provided selector cause the callback to be executed. This may facilitate the entire removal of the JavaScript “filtering code” that traditionally resided in the “classic” mutation events handler. Now, the callback is only invoked when necessary, saving computation cycles and making the library more efficient.

Limitations

watchSelector[All] are versatile enough to replace the use of the following Mutation events (though not on all Node types; exclusions noted). Excluded use cases for all but Text Nodes are not expected to be prevalent on the web.

  • DOMAttributeNameChanged (i.e., using the [attribute] selector)
  • DOMAttrModified (i.e., using the [attribute=value] selector)
  • DOMNodeInserted
    • (except for: Text, Comment, CDATASection, DocType, EntityRef, PI)
  • DOMNodeInsertedIntoDocument
    • (except for: Text, Comment, CDATASection, DocType, EntityRef, PI)
  • DOMNodeRemoved
    • (except for: Text, Comment, CDATASection, DocType, EntityRef, PI)
  • DOMNodeRemovedFromDocument
    • (except for: Text, Comment, CDATASection, DocType, EntityRef, PI)
  • DOMSubtreeModified (i.e., using the * selector)
  • (except for: Text, Comment, CDATASection, DocType, EntityRef, PI)

Not replaced:

  • DOMCharacterDataModified

It’s worth noting that a similar API designed around the XPath language would satisfy all of the preceding mutation events, due to its capability of selecting more than just elements and attributes.

Drawbacks:

  • "old values" (previous values) for DOMAttrModified are not provided
  • No guarantee of order (in the Asynchronous API)
  • Depending on the selector, web developers may not get notified of certain types of DOM mutations. For example, the * selector won't catch tree mutations that are simply element movements in the tree (as opposed to new elements/elements removed).
  • Some selectors may cause worse performance than the vanilla mutation events (e.g., * selector in the synchronous API).