Warning:
This wiki has been archived and is now read-only.
MutationReplacement
NOTE: This document is no longer maintained because the Web Applications Working Group was closed in October 2015 and its deliverables transferred to the Web Platform Working Group.
Contents
- 1 Overview
- 2 Background
- 3 Goals
- 4 Scenarios
- 4.1 Keeping updated statistics of page DOM
- 4.2 Widgetify dynamically created markup
- 4.3 Poor man's XBL
- 4.4 For browser extensions (not for web page authors)
- 4.5 Monitoring aria-activedescendant
- 4.6 Mutations to non-primary DOM trees
- 4.7 Avoid notifications for DOM tree "thrashing"
- 4.8 Avoid excessive notifications when the DOM is modified "in bulk"
- 4.9 Avoid possible side-effects from simple DOM operations
- 5 MutationTarget (A Mozilla Proposal)
- 6 Commentary on the Mozilla Proposal
- 7 NodeWatch (A Microsoft Proposal)
- 8 Appendix
Overview
Mutation Events are widely acknowledged as “slow” in terms of the real performance degradation that occurs on websites that use them heavily for tracking changes to the DOM. I propose an alternative solution that allows implementations to provide equivalent functionality without the performance cost.
By Travis Leithead, Jan 2010, send comments to travil AT microsoft dot com
Background
The mutation events as defined by DOM Level 2 Events (and as deprecated in DOM Level 3 Events) suffer from a number of real-world performance problems that stem from their original design characteristics.
The first problem is that mutation events are verbose – they are required to fire excessively for every change (they must be implemented as a synchronous event). Based on several empirical tests and a discussion of the analysis done on the www-dom mailing list, Maciej Stachowiak even concluded: "I think the data presented shows the performance characteristics of mutation events to be unacceptable in current browsers to endorse them as a common technique."
Example from John Resig (jQuery): "Firefox, for example, when it realizes that a mutation event has been turned on, instantly goes into an incredibly-slow code path where it has to fire events at every single DOM modification. This means that doing something like .innerHTML = "foo" where it wipes out 1000 elements would fire, at least 1000 + 1 events (1000 removal events, 1 addition event)."
A solution to this problem is to batch or group mutation events into a set of changes that the author cares about (e.g., as mentioned by Sean Hogan). They could also be fired asynchronously. François Remy suggested a filtering approach based on CSS Selectors or XPath. The approach is to provide a filter that would avoid firing listeners if the selector didn’t match the event’s target/currentTarget:
[mutation_event_target].addFilteredEventListener([mutation_event_name], [filter], function(e) {...}, true/false)
The second problem is mutation events, being events, suffer from the implementation overhead of event propagation (creating the event object, capturing it through the DOM and then bubbling it in some cases, triggering multiple listeners to be fired). Because listeners may further alter the DOM, a listener is therefore not guaranteed to be notified of the original change, but may get the updated value after another listener has changed it.
Jonas Sicking suggested that a replacement to mutation events be something that doesn’t use events because of these issues.
A third problem is that mutation events must carry before-and-after information related to DOM state changes and that this comes at a performance cost. Boris Zbarsky suggests that this information is rarely needed by web applications in practice. He also describes the implementation cost associated with providing the before and after state. Additionally, David Bolter cautions that some before-and-after state information is useful, especially in the context of text changes.
One potential opportunity is to not provide the before-and-after state information (for attribute/text nodes). For scenarios where a web developer may need to be pre-notified of a change (to get the "before" value), the ECMAScript 5 defineProperty API can be used to replace the setters for the various attribute accessors or text data setters. For the common case, notification of the change may be enough.
Goals
- Compatibility with existing mutation events - Do not modify the calling syntax or semantics of existing mutation events
- Changing the way that mutation events behave, even slightly, can affect web compatibility for existing code using mutation events.
- Performance - Fast mutation events decrease page run-time cost by (a significant amount) across all scenarios
- Provide an Asynchronous API only - A synchronous API can be created if desired, but in order to preserve these notifications to be as fast as possible, we're beginning with Async.
Scenarios
The following 4 scenarios were collected from various W3C members and posted to www-dom by Charles McCathieNevile:
Keeping updated statistics of page DOM
The most popular examples of these today are probably selector engine libraries. They can benefit from scanning the DOM tree once into a cache, and then let the cache be updated/invalidated based on mutation event Now hopefully querySelectorAll() support will void this particular usage, but there will always be other uses needing to do similar things, be it keeping an updated count of text length or doing something selector based not supported by QSA, f ex reverse selector mapping.
Widgetify dynamically created markup
Widgets may be used to override or decorate page markup cooperatively (the markup knows about it and uses special tagging to trigger it) or silently (a page script looks for patterns in the markup to direct widget creation at). Usually this is done as a page-wide search-and-replace at page load. Though, when markup is created dynamically throughout the page's lifetime, f ex through click events that "expand a pane", it is hard to do the silent/uncooperative replacement. Mutation events let us solve this and trigger widget creation when any new matching markup appears.
NOTE: Generally, widgets are required to be instantiated immediately after the appropriate state has been set in the DOM because the widget’s functionality is potentially expected immediately in the next statement of code. This scenario (immediate widget hookup) is not supported by this proposal—rather the ECMAScript 5 defineProperty is a better fit for that scenario (a synchronous hookup).
Poor man's XBL
(from: Robin Berjon in conversation with Jonas Sicking) [...] if browsers would give us XBL2, mutation events would be a whole lot less needed. (Yes Jonas, you know at whom I'm looking). One thing that mutation events were used a lot for back in the old days of ASV (that supported them early on, IIRC in v3) was to add behaviour through decoration. Basically you could add a foo:draggable="true" on an element and know that a JS library would detect that change and make your element draggable. It's the poor man's XBL really.
For browser extensions (not for web page authors)
(from: Sergey) 1) If I am the author to the scripts that modify document, then I am indeed aware of what gets changed. If I am not the author, I shall then not have been notified on the change. The use cases such as "debugger" do not count here [...]. 2) I can see Mutation Events as the extension point that enables implementation of the technologies that are not available in the browser. However this is not a "normal" usecase that web browsers are here to face.
Monitoring aria-activedescendant
(use-case proffered by David Bolter): One potential use case (comes from) WAI-ARIA (aria-activedescendant). […] The idea is that modifications to the aria-activedescendant attribute (DOMAttrModified) are expected to be handled by the web developer, and the visual appearance of focus to be restyled. Related exploration I did for the W3C is here:
Mutations to non-primary DOM trees
Several use cases made explicit in a reply by Jonas Sicking: "(…) I'm also concerned about situations where Nodes that are disconnected from the Document (are) mutated. And about when nodes that are in an undisplayed part of the document (are) mutated. And when Documents that aren't displayed are mutated (such as documents from XMLHttpRequest or DOMParser). And when attributes that don't affect layout are mutated.
The following three scenarios were suggested by Giovanni Campagna:
Avoid notifications for DOM tree "thrashing"
I set an attribute, the listener fires I set the attribute again back to the previous value, the listener fires again In that case, I may want to ignore the listener completely, because the attribute didn't really change.
Avoid excessive notifications when the DOM is modified "in bulk"
I have a long list of element that I want to add to another one. Every appendChild() will trigger the modification listener, again and again. In this case, I may want to use the "batch" feature, and call it just once, in particular if the listener is watching only for the elements present under the subtree, rather than the modification itself.
Avoid possible side-effects from simple DOM operations
I'm sure there is code that expects (or requires) that after a "setAttribute" the only thing that changed in the DOM was the actual attribute set. Preventing modification listeners within "protected" code helps this.
MutationTarget (A Mozilla Proposal)
This section describes the API design proposed by Jonas Sicking of Mozilla in June 2009
Design Goals
(From a clarifying email sent after the original proposal)
1. Listeners should be notified in the order that mutations take place. So, for example, if a node is first inserted into the DOM, and then an attribute is set on the node, the notifications should happen in that order, and never ever be notified first about the attribute change, and then about the node insertion.
This goal is not fulfilled by DOM Mutation Events. Consider the following:
- Two event listeners are registered for the DOMNodeInserted and one for DOMAttrModified
- A node is inserted.
- The implementation fires a DOMNodeInserted.
- The first DOMNodeInserted listener fires. It sets an attribute on the inserted node.
- The implementation fires DOMAttrModified.
- The DOMAttrModified listener is notified.
- The second DOMNodeInserted listener fires.
As you see the, second DOMNodeInserted listener is notified after the DOMAttrModified listener. This is what I wanted to avoid.
2. Notifications should be synchronous. If someone calls .setAttribute(...), and there is a listener that updates a bunch of state based on the "attribute has changed" mutation notification, that the caller of setAttribute can rely on that all state is up to date by the time the setAttribute(...) returns.
Unfortunately these two goals are incompatible. I can't fully satisfy them both. So the sacrifice I made was that calls that happen inside of a notification no longer satisfy the second goal. This is accomplished by the flag and queue described in the algorithm [below].
Interface Definition
// interface name courtesy of Travis Leithead :-) [NoInterfaceObject] interface MutationTarget { addAttributeChangedListener(NodeDataCallback); addSubtreeAttributeChangedListener(NodeDataCallback); addChildlistChangedListener(NodeDataCallback); addSubtreeChangedListener(NodeDataCallback); addTextDataChangedListener(NodeDataCallback); removeAttributeChangedListener(NodeDataCallback); removeSubtreeAttributeChangedListener(NodeDataCallback); removeChildlistChangedListener(NodeDataCallback); removeSubtreeChangedListener(NodeDataCallback); removeTextDataChangedListener(NodeDataCallback); }; Document implements MutationTarget; Element implements MutationTarget; DocumentFragment implements MutationTarget; [NoInterfaceObject] interface NodeDataCallback { void handleEvent(in Node node); };
These are *not* DOM-Event listeners. No DOM Events are created, there are no capture phases or bubbling phases. Instead you register a listener on the node you are interested in being notified about, and will get a call after a mutation takes place.
The Listeners
The listeners are called as follows:
AttributeChanged
Called when an attribute on the node is changed. This can be either an attribute addition, removal, or change in value.
If setAttribute is called the 'AttributeChanged' listener is always called, even if the attribute is given the same value as it already had. This is because it may be expensive to compare if the old and new value was the same, if the value is internally stored in a parsed form.
[Jonas Followup: So I ran some numbers. About 10-20% of all attribute setting is for the same value that the attribute already has. That's quite higher than I had expected. I went to some news sites, some mozilla.org pages, gmail and google calendar.
One solution would be to leave it undefined. This way the implementation could do whatever is most performant. For example, in gecko, setting the style attribute would require a slow serialization of style data to determine if a change occurred, whereas setting a data-* attribute only requires a quick string compare.]
SubtreeAttributeChanged
As for 'AttributeChanged', but called not just for attribute changes to the node, but also to any descendants of the node.
ChildlistChanged
Called if one or more children are added or removed to the node. For example, when innerHTML is set, replacing 10 existing nodes with 20 new ones, only one call is made to the 'ChildlistChanged' listeners on that node.
SubtreeChanged
Same as 'ChildlistChanged', but called not just for children added/removed to the node, but also to any descendants of the node.
TextDataChanged
Called when the data in a Text, CDATASection or ProcessingInstruction, is changed on the node or any of its descendants.
Algorithm
The exact algorithm would go something like this:
The implementation contains a global list, pendingCallbacks. Each item in pendingCallbacks is a tuple: <callback, node>. The implementation also contains a flag, notifyingCallbacks, which initially is set to false.
If an attribute is changed on node N, perform the following steps:
- For each 'AttributeChanged' callback, C, registered on N, add an item to the end of pendingCallbacks with the value <C, N>
- For each 'SubtreeAttributeChanged' callback, C, registered on N, add an item to the end of pendingCallbacks with the value <C, N>
- Set N to N's parentNode.
- If N is non-null, go to step 2.
- If notifyingCallbacks is set to true, or pendingCallbacks is empty, abort these steps.
- Set notifyingCallbacks to true.
- Remove the first tuple from pendingCallbacks.
- Call the handleEvent function on the callback in the tuple, with the 'node' argument set to the node from the tuple.
- If pendingCallbacks is not empty, go to step 7.
- Set notifyingCallbacks to false.
A similar algorithm is run when children are added and/or removed from an element/ document/ document-fragment, just replace 'AttributeChanged' with 'ChildlistChanged', and ‘SubtreeAttributeChanged', with'SubtreeChanged'.
And when Text/CDATASection/ProcessingInstruction nodes have their contents modified the same algorithm is run, but without step 1, and with 'SubtreeAttributeChanged' replaced with 'TextDataChanged'.
Benefits and Drawbacks
There's a few of properties here that are desirable, and one that is sub-optimal.
First of all no notifications are done until after the implementation is done with the mutations. This significantly simplifies the implementation since all critical work can be done without worrying about script performing arbitrary operations in the middle.
Second, all callbacks are notified in the order mutations happen in the tree, even when another callback performs further mutations. So if a callback for 'ChildlistChanged' sets an attribute on the newly inserted node, the other callbacks are still going to get first notified about the node being inserted, and then about it having an attribute set. This is thanks to the notifyingCallbacks flag.
Third, these notifications should be significantly faster. Since there are no DOM Events, there is no need to build propagation chains. There's also no need to recreate the old attribute value, or to fire one event for each node if multiple nodes are added and removed.
The only undesirable feature is that code that mutates the DOM from inside a callback, say by calling setAttribute, can't rely on all callbacks having been notified by the time that setAttribute returns.
This is unfortunately required if we want the second desirable property listed above.
The only feature of mutation events that this doesn't provide is the functionality that the DOMNodeRemovedFromDocument/DOMNodeInsertedIntoDocument. I believe that these are implemented in some browsers. I don't know how these browsers would feel about dropping support without supplying some sort of alternative. If needed we could add the following:
[Supplemental, NoInterfaceObject] interface MutationTarget { addInDocumentChangedDocumentListener(NodeDataCallback); removeInDocumentChangedDocumentListener(NodeDataCallback); };
But I'm not sure that there is web-dependency on this, or good use cases.
/ Jonas
Commentary on the Mozilla Proposal
I agree with most of the major design goals of the Mozilla proposal. However there are two aspects of the proposal that I dislike:
- There is no provision for grouping or consolidating events. A Filtering approach allows for a more general-purpose design where a web author may indicate the specific circumstances under which listeners should be triggered, and in the extreme case, the filter can be general enough to cover all the use cases that a non-filtering approach would also cover.
- There are many APIs. For simplicity’s sake, it makes sense to reduce the number of APIs especially when they are very similar in nature. It is also desirable to have fewer APIs from a author-perspective (fewer things to remember). This requirement is, of course, equally relevant to a single API design that is very complex in its parameter list.
- If an asynchronous API design is included, greater implementation performance can be achieved by not requiring any logic to take place as modifications are performed to attributes or nodes. The algorithm(s) above are required to run on each tree modification. This can be avoided with an asynchronous design (either in addition to or replacing a synchronous design).
Mike Wilson also offered feedback that a filtering approach was missing. He states that filtering could be built on top of the low-level provided APIs, but that some native support for filtering would help increase the performance potential.
Giovanni Campagna offers feedback on the Mozilla proposal by suggesting fewer APIs (per #2 above) as well as a possible asynchronous solution based on the HTML5 event queue mechanism. The API consolidation arguments center around the problem of adding a new mutation API for each type of node that could (possibly) be added to the DOM in the future. The feedback around the asynchronous design, as well as Jonas’ responses clarify an addition use case for the design of the Mozilla proposal:
The listeners are intended to be executed synchronously, but not during complex API calls like innerHTML.
"I think we want the notifications to fire by the time that the function that caused the DOM mutation returned. So in code like:
myNode.innerHTML = "<b>hello world!</b>"; doStuff();
By the time doStuff is called all the notifactions have fired and updated whatever state that they need to update."
NodeWatch (A Microsoft Proposal)
The following proposal is a second draft of the proposal currently on the W3C Events Wiki page, based on initial feedback received from Doug Schepers.
Due to its close relationship with the W3C Selectors API Level 1, the API’s naming convention and aspects of its design are inspired from Selectors API semantics.
This proposal for a fast mutation events replacement is "watchSelector". watchSelector is an asynchronous API for registering callback notifications for when document mutations occur. At its core, it relies on the browser’s CSS Selector engine to provide a "filter" that is used to scope the document mutations that the author is interested in listening to. After the author-provided timeout period elapses and there have been mutations to the document that match the provided selector, the listener is called back with a (static) NodeList of the net changes to the document that have occurred.
Interface Definition
[NoInterfaceObject] interface NodeWatch { long watchSelector(in DOMString selectors, in SelectorNodeListCallback callback, in optional unsigned long minimumFrequency) raises(DOMException=SYNTAX_ERR, DOMException=NAMESPACE_ERR, DOMException=INDEX_SIZE_ERR); long watchCharacterData(in CharacterDataCallback callback, in optional unsigned long minimumFrequency); void unwatchSelector(in long handle); void unwatchCharacterData(in long handle); }; Document implements NodeWatch; Element implements NodeWatch; DocumentFragment implements NodeWatch; [Callback=FunctionOnly, NoInterfaceObject] interface SelectorNodeListCallback { void handleCallback(in NodeList newMatches, in NodeList unMatched, in long handle); }; [Callback=FunctionOnly, NoInterfaceObject] interface CharacterDataCallback { void handleCharacterCallback(in Element lowestCommonAncestor, in long handle); }; typedef domL3Core::Document Document; typedef domL3Core::Element Element; typedef domL3Core::DocumentFragment DocumentFragment; typedef domL3Core::NodeList NodeList; typedef domL3Core::DOMException DOMException;
watchSelector
Registers an asynchronous callback for notifications of document mutations that match the provided group of selectors and that occur during the provided minimum frequency period. Only results that fall within the subtree of the Node the API is called on are returned. The tuple <callback + post-processed(selectors string)> on a given node constitute a unique "node watcher." Only one unique node watcher may be registered on a given node at a time. Multiple calls to watchSelector providing the same node watcher are silently ignored.
- Parameters
- selectors – a group of selectors, processed identically to those specified in the Selectors API Level 1 spec section 6.2 and 6.3
- callback – a SelectorNodeListCallback object (or JavaScript function in ECMAScript) to be invoked after the minimum frequency has expired, whenever any new matches to the provided group of selectors occur or whenever nodes that previously matched the group of selectors no longer match.
- minimumFrequency - the elapsed time (in miliseconds) to wait before (re-)checking for any changes to the document. When not provided, the default is 0.
- Return value:
- handle – a long value that represents the scheduled node watcher and may be used to unwatch the scheduled node watcher at any time.
- Exceptions:
- SYNTAX_ERR may be thrown if an invalid [group of] selectors is presented in the first parameter
- NAMESPACE_ERR may be thrown if a namespace prefix needs to be resolved (according to section 6.3 of the Selectors API Level 1 spec.
- INDEX_SIZE_ERR occurs if the provided minimumFrequency parameter is a negative value.
watchCharacterData
Registers an asynchronous callback for notifications of character data node mutations for the given node or its sub-tree that occur during the provided minimum frequency period. A character data node mutation is defined as a delta between the node’s textContent (i.e., DOM L3 Core definition), or if node is the Document, then the documentElement’s textContent. The callback constitutes a unique "character data watcher" for this API. Only one unique character data watcher may be registered on a given node at a time. Multiple calls to watchCharacterData providing the same callback are silently ignored.
- Parameters
- callback – a CharacterDataCallback object (or JavaScript function in ECMAScript) to be invoked after the minimum frequency has expired, whenever the content of any character data nodes (either direct children or within the node’s subtree) have changed.
- minimumFrequency - the elapsed time (in miliseconds) to wait before (re-)checking for any changes to the document.
- Return value:
- handle – a long value that represents the scheduled character data watcher and may be used to unwatch the scheduled character data watcher at any time.
- Exceptions:
- No exceptions
unwatchSelector
Un-registers an asynchronous callback for a given [group of] selectors. If no callback was registered than this method simply returns.
- Parameters
- handle – the unique handle to a previously scheduled nodeWatcher, obtained as the return value from watchSelector or from the 3rd parameter of the watchSelector callback function
- Return value:
- no return value
- Exceptions:
- No exceptions
unwatchCharacterData
Un-registers an asynchronous callback for character data mutations. If no callback was registered than this method simply returns.
- Parameters
- handle – the unique handle to a previously scheduled character data watcher, obtained as the return value from watchCharacterData or from the 2nd parameter of the watchCharacterData callback function
- Return value:
- no return value
- Exceptions:
- No Exceptions
handleCallback
The signature of the callback function to provide to watchSelector and unwatchSelector. In ECMAScript, the provided Function should provide parameters for each of the three values passed to handleCallback. The callback is invoked with its ‘this’ value set to the Node used to register the callback.
- Parameters
- newMatches – a static list of Nodes (the list won’t dynamically change with future DOM modifications) that newly match the relatedSelectors since the last time the callback was issued, or since the callback was first registered, whichever came last. The list of Nodes will only contain Element node types and will be in document order.
- unMatched – a static list of Nodes that previously matched the relatedSelectors (since the last time the callback was issued, or since the callback was first registered, whichever came last) but presently do not match the relatedSelectors. The list of nodes will only contain Element node types and will be in document order.
- handle – a long value that represents the scheduled node watcher and may be used to unwatch the scheduled node watcher at any time.
- Return value
- None
handleCharacterCallback
The signature of the callback function to provide to watchCharacterData and unwatchCharacterData. In ECMAScript, this is a Function that should provide one parameter to receive the lowestCommonAncestor. The callback is invoked with its ‘this’ value set to the Node used to register the callback.
- Parameters
- lowestCommonAncestor – an Element that represents the lowest common ancestor of all the CharacterData nodes that were changed since the last time the callback was issued, or since the callback was first registered, whichever came last.
- handle – a long value that represents the scheduled character data watcher and may be used to unwatch the scheduled character data watcher at any time.
- Return value
- None
Lifetime Management
The lifetime of node and character data watchers (registrations of watchSelector or watchCharacterData), are managed by the lifetime of the object they are registered on.
If the object is moved from one location in the DOM tree to another, its node watchers/ character data watchers remain valid and apply to the new tree location. If the object is moved into a DocumentFragment or is disconnected from the primary DOM tree, its node watchers/ character data watchers continue to apply to that node and any subtree available to that node.
If the object is cloned, its node watchers/ character data watchers are not cloned.
If the object is no longer referenced anywhere and has no outstanding downloads or other internal references then it should clean up its node/character data watchers to ensure that the watchers don’t continue to occupy memory or consume CPU cycles.
Algorithms
The following algorithms illustrate the expected behavior of the APIs defined above, and are guidelines for implementation.
Let globalNodeWatchers
be a list of tuples <Node, Selector, Callback, Handle, Matches, Frequency> where Node is the Node on which watchSelector is invoked, Selector is the post-validated 1st parameter to watchSelector, Callback is the function provided in the 2nd parameter to watchSelector, Handle is a unique timer handle, Matches is a list of Elements, initially empty, and Frequency is the post-validated 3rd parameter to watchSelector. The globalNodeWatchers list is initially empty.
Let globalCharacterDataWatchers
be a list of tuples <Node, Callback, Handle, Frequency> where Node is the Node on which watchCharacterData is invoked, Callback is the function provided in the 1st parameter to watchCharacterData, Handle is a unique timer handle, and Frequency is the post-validated 2nd parameter to watchCharacterData. The globalCharacterDataWatchers list is initially empty.
Note: The timer queue referred to in the following algorithms is the global timer queue running on the UI thread (not truly asynchronous) that is the same queue used for calls to window.setInterval and window.setTimeout.
Let internalCheckNodeWatchers
be a function that takes as input a tuple from the globalNodeWatchers list. When invoked, run the following steps:
- Let
previousResults
be the list from tuple.Matches. - Let
currentResults
be the result of invoking NodeSelector::querySelectorAll.call(tuple.Node, tuple.Selector); - Let
notMatched
be a new empty list of Elements. - Let
newlyMatched
be a new empty list of Elements. - For each Element prevElem in previousResults and each Element currElem in currentResults (in document order iteration):
- If prevElem and currElem are the same element, then remove prevElem from the previousResults list and remove currElem from the currentResults list.
- If prevElem is not found in currentResults, then add it to notMatched.
- If currElem is not found in previousResults, then add it to newlyMatched.
- If both notMatched and newlyMatched are empty lists, then goto step 9.
- Invoke tuple.Callback.call(tuple.Node, newlyMatched, notMatched, tuple.Handle).
- Replace tuple.Matches with currentResults.
- (Re-)start the timer using the same tuple.Handle to call internalCheckNodeWatchers with the tuple tuple after tuple.Frequency miliseconds.
When watchSelector
is invoked, run these steps:
- If the minimumFrequency parameter is provided, validate that it is >= zero. If not, throw an INDEX_SIZE_ERR. If the minimumFrequency is not provided, assign zero to the minimumFrequency.
- Let
validatedSelector
be the result of validating the selectors parameter according to the Selectors API Level 1 spec section 6.2 and 6.3. If the selector does not validate, throw a SYNTAX_ERR or NAMESPACE_ERR whichever applies. - Let
n
be the Node on which watchSelector was invoked (the ‘this’ value). - For each entry in the globalNodeWatchers list, validate that an entry with <n, validatedSelector, callback, *, *, *> does not already exist. If an entry is found, abort these steps.
- Let
resultlist
be the result of invoking NodeSelector::querySelectorAll.call(n, validatedSelector); - Let
timerHandle
be the result of creating a recurring timer to invoke internalCheckNodeWatchers every minimumFrequency miliseconds. - Create a new entry at the end of the list of globalNodeWatchers adding <n, validatedSelector, callback, timerHandle, resultlist, minimumFrequency>.
- Immediately invoke internalCheckNodeWatchers providing the newly created tuple as the parameter.
Let textDirty
be a flag present on each Text, and CDataSection object, initially set to false. Assume that whenever the nodeValue of these objects are changed, this flag is set to true. Parsing of the document during the document loading does not set textDirty flags to true, only DOM operations after the document has finished loading.
Let internalCheckCharacterDataWatchers
be a function that takes as input a tuple from the globalCharacterDataWatchers list. When invoked, it runs the following steps:
- Let
baseElem
be tuple.Node. - If baseElem is a DOCUMENT_NODE, then let
baseElem
be the document’s documentElement instead. - Let LCA be null
- For each Text or CDataSection node
text
in baseElem’s subtree (visiting the nodes in document order traversal):- If text.textDirty is true and LCA is null, let
LCA
be the parent Element of text. - If text.textDirty is true and LCA is not null, then let
LCA
be the Element shared by both LCA and the parent Element of text. - Reset text.textDirty to false.
- If text.textDirty is true and LCA is null, let
- If LCA is null, then goto step 7.
- Invoke tuple.Callback.call(tuple.Node, LCA, tuple.Handle);
- (Re-)start the timer based on tuple.Handle to call internalCheckCharacterDataWatchers with the tuple tuple after tuple.Frequency miliseconds.
When watchCharacterData
is invoked, run these steps:
- If the minimumFrequency parameter is provided, validate that it is >= zero. If not, throw an INDEX_SIZE_ERR. If the minimumFrequency is not provided, assign zero to the minimumFrequency.
- Let
n
be the Node on which watchCharacterData was invoked (the ‘this’ value). - For each entry in the globalCharacterDataWatchers list, validate that an entry with <n, callback, *, *> does not already exist. If an entry is found, abort these steps.
- Let
timerHandle
be the result of creating a recurring timer to invoke internalCheckCharacterDataWatchers every minimumFrequency miliseconds. - Create a new entry at the end of the list of globalCharacterDataWatchers adding <n, callback, timerHandle, minimumFrequency>
- Start the timer using timerHandle to call internalCheckCharacterDataWatchers providing the newly created tuple as the parameter.
Usage Example
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 one they need to monitor state changes to keep track of new nodes for certain tasks (e.g., to perform spell-checking, grammar checking, DOM UI-annotation or state change updates based on DOM attribute or element presence or removal).
In these scenarios, the current state of the “watching” code can be easily initialized and kept up to date by using the watchSelector API. Text change notifications (for Text and CDataSection objects) can be monitored by using watchCharacterData API. watchCharacterData while not providing a filtering mechanism, does consolidate all the text mutations into a single asynchronous notification.
In this UI-helper example, the web developer creates a callback to synchronize phone-numbers based on the presence of the attribute “toolXYZ-phone-helper.”
var monitorWidget = function (added, removed, handle) { // TODO: for each ‘added’ node, add appropriate DOM // infrastructure to support the phone helper code // TODO: for each element in ‘removed’, cleanup the // phone helper logic to allow the node to be GC’d // TODO: Use ‘handle’ to call unwatchSelector in case of // configuration state change } var widgetSig = “[toolXYZ-phone-helper]”; // Notify my monitor code as soon as the page completes a // synchronous update (use the default minimumFrequency) document.watchSelector(widgetSig, monitorWidget);
Then as document mutations occur, only the modifications that fall into or out of the provided selector cause the callback to be executed, and only after the synchronous script has finished executing.
Replacing Traditional Mutation Events
watchSelector and watchCharacterData are versatile enough to replace the use of the following Mutation events (though not on all Node types [exclusions noted]). Excluded use cases are not expected to be prevalent or common scenarios on the web.
Mutation Event | Example usage | Excluded Nodes (no notifications available) |
DOMAttributeNameChanged | watchSelector with an [attribute] selector | none |
DOMAttrModified | using watchSelector with the [attribute=value] selector | none |
DOMNodeInserted | using watchSelector with a simple selector, or using watchCharacterData | Comment, DocType, EntityRef, PIs |
DOMNodeInsertedIntoDocument | using watchSelector or watchCharacterData on the document of interest | Comment, DocType, EntityRef, PIs |
DOMNodeRemoved | using watchSelector with a simple selector, or using watchCharacterData | Comment, DocType, EntityRef, PIs |
DOMNodeRemovedFromDocument | using watchSelector or watchCharacterData on the document of interest | Comment, DocType, EntityRef, PIs |
DOMSubtreeModified | using watchSelector with the * selector, or using watchCharacterData | (Movements of elements within the sub-tree will not be detected), Comment, DocType, EntityRef, PIs |
DOMCharacterDataModified | using watchCharacterData | Comment, DocType, EntityRef, PIs |
Shortcomings:
- Previous values for attribute values or character data are not provided. Web developers can cache old values and compare then to new values if this scenario is required.
- Attribute value changes must be detected by using an attribute value selector. Web developers can change the attribute value selector at each callback to affect a generic change notification pattern if desired. In general, the Microsoft proposal does not optimize around general "node changed" notifications.
- The ordering of document mutations is not preserved
- When using selectors that do not imply document structural relationships (e.g., “div”) movements of the elements within the document will not trigger callbacks. If this scenario is needed, supply a selector that enforces a particular document structure (e.g., “div>div”)
Appendix
Design Trade-offs
Initially, the design for the unwatchSelector APIs was to take a Selector and Callback to un-register the timer from firing. The reason both were needed is because we wanted to allow a single callback handler to be used with (potentially) multiple different selectors. Therefore the selector+callback would constitute a unique registration.
The issue that arises is: what selector do you use for the registration? The “initial” selector, the “cleaned up” selector, or the “internal representation (if any)” of the selector? For example, if watchSelector is registered via the selector text “ div > div “ (spaces intentional), could it be unregistered via “div>div”? If the answer is no, then one can imagine how insignificant typos could cause author-grief. An attempt was made in the initial design to return a selector (the initial selector text) to the callback that could be used (in conjunction with the callback reference) to unregister that listener—however, this involved saving two selector states (because otherwise a selector would have to be reliably generated that matched the input selector text).
A second problem arose: Will (“div > div”, callback) and (“div>div”, callback) register two unique callbacks, or just one (since the selectors are functionally equivalent). In the original proposal this would register two callbacks, but this is undesireable.
The solution was to use a “handle” approach, making the API design more similar to setTimeout, as well as providing an exclusive token in the callback that could be used to un-register the listener (even if the return value from msWatchSelector was ignored.