Most Efficient Method of Detecting/Monitoring Dom Changes

Most efficient method of detecting/monitoring DOM changes?

http://www.quirksmode.org/js/events/DOMtree.html

jQuery now supports a way to attach events to existing and future elements corresponding to a selector: http://docs.jquery.com/Events/live#typefn

Another interesting find - http://james.padolsey.com/javascript/monitoring-dom-properties/

What is the most efficient way of detecting when an element with a particular ID is inserted in the DOM

It turns out there is a more efficient way to detect when a particular node is inserted, by using CSS animation events.

You can define an unnoticeable animation to the class you are listening for, lets say widget_inserted:

@keyframes widget_inserted {  
from { z-index: 1; }
to { z-index: 1; }
}

.widget {
animation-duration: 0.001s;
animation-name: widget_inserted;
}

I've chosen the z-index property because in most cases it should have virtually no added effects on the element which is inserted. Combined with a very short animation duration, these added CSS properties should produce no additional changes to the original document.

So now if you attach an animationstart event listener to the document, you should be able to catch the exact moment when an element with the widget class is inserted:

document.addEventListener('animationstart', function(event) {
if (event.animationName == 'widget_inserted') {
...
}
});

You can view a live demo here

How do I monitor the DOM for changes?

See "MutationEvent" elements in here:
https://developer.mozilla.org/en/DOM/DOM_event_reference but those are deprecated.

jQuery now features a way to attach events to existing AND future elements corresponding to a selector:
http://docs.jquery.com/Events/live#typefn

It may be a trick you could use for lack of proper DOM Node modification info.

Detecting DOM changes with puppeteer and MutationObserver

Maybe page.exposeFunction() is the simplest way:

'use strict';

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.example.com/');

await page.exposeFunction('puppeteerLogMutation', () => {
console.log('Mutation Detected: A child node has been added or removed.');
});

await page.evaluate(() => {
const target = document.querySelector('body');
const observer = new MutationObserver( mutations => {
for (const mutation of mutations) {
if (mutation.type === 'childList') {
puppeteerLogMutation();
}
}
});
observer.observe(target, { childList: true });
});

await page.evaluate(() => {
document.querySelector('body').appendChild(document.createElement('br'));
});

})();

But page.waitForSelector() (or some other waitFor... methods) in some cases can also suffice:

'use strict';

const puppeteer = require('puppeteer');

(async () => {
const browser = await puppeteer.launch({ headless: false });
const page = await browser.newPage();
await page.goto('https://www.example.com/');

await page.evaluate(() => {
setTimeout(() => {
document.querySelector('body')
.appendChild(document.createElement('br')).className = 'puppeteer-test';
}, 5000);
});

await page.waitForSelector('body br.puppeteer-test');

console.log('Mutation Detected: A child node has been added or removed.');
})();

Detect changes in the DOM

2015 update, new MutationObserver is supported by modern browsers:

Chrome 18+, Firefox 14+, IE 11+, Safari 6+

If you need to support older ones, you may try to fall back to other approaches like the ones mentioned in this 5 (!) year old answer below. There be dragons. Enjoy :)


Someone else is changing the document? Because if you have full control over the changes you just need to create your own domChanged API - with a function or custom event - and trigger/call it everywhere you modify things.

The DOM Level-2 has Mutation event types, but older version of IE don't support it. Note that the mutation events are deprecated in the DOM3 Events spec and have a performance penalty.

You can try to emulate mutation event with onpropertychange in IE (and fall back to the brute-force approach if non of them is available).

For a full domChange an interval could be an over-kill. Imagine that you need to store the current state of the whole document, and examine every element's every property to be the same.

Maybe if you're only interested in the elements and their order (as you mentioned in your question), a getElementsByTagName("*") can work. This will fire automatically if you add an element, remove an element, replace elements or change the structure of the document.

I wrote a proof of concept:

(function (window) {
var last = +new Date();
var delay = 100; // default delay

// Manage event queue
var stack = [];

function callback() {
var now = +new Date();
if (now - last > delay) {
for (var i = 0; i < stack.length; i++) {
stack[i]();
}
last = now;
}
}

// Public interface
var onDomChange = function (fn, newdelay) {
if (newdelay) delay = newdelay;
stack.push(fn);
};

// Naive approach for compatibility
function naive() {

var last = document.getElementsByTagName('*');
var lastlen = last.length;
var timer = setTimeout(function check() {

// get current state of the document
var current = document.getElementsByTagName('*');
var len = current.length;

// if the length is different
// it's fairly obvious
if (len != lastlen) {
// just make sure the loop finishes early
last = [];
}

// go check every element in order
for (var i = 0; i < len; i++) {
if (current[i] !== last[i]) {
callback();
last = current;
lastlen = len;
break;
}
}

// over, and over, and over again
setTimeout(check, delay);

}, delay);
}

//
// Check for mutation events support
//

var support = {};

var el = document.documentElement;
var remain = 3;

// callback for the tests
function decide() {
if (support.DOMNodeInserted) {
window.addEventListener("DOMContentLoaded", function () {
if (support.DOMSubtreeModified) { // for FF 3+, Chrome
el.addEventListener('DOMSubtreeModified', callback, false);
} else { // for FF 2, Safari, Opera 9.6+
el.addEventListener('DOMNodeInserted', callback, false);
el.addEventListener('DOMNodeRemoved', callback, false);
}
}, false);
} else if (document.onpropertychange) { // for IE 5.5+
document.onpropertychange = callback;
} else { // fallback
naive();
}
}

// checks a particular event
function test(event) {
el.addEventListener(event, function fn() {
support[event] = true;
el.removeEventListener(event, fn, false);
if (--remain === 0) decide();
}, false);
}

// attach test events
if (window.addEventListener) {
test('DOMSubtreeModified');
test('DOMNodeInserted');
test('DOMNodeRemoved');
} else {
decide();
}

// do the dummy test
var dummy = document.createElement("div");
el.appendChild(dummy);
el.removeChild(dummy);

// expose
window.onDomChange = onDomChange;
})(window);

Usage:

onDomChange(function(){ 
alert("The Times They Are a-Changin'");
});

This works on IE 5.5+, FF 2+, Chrome, Safari 3+ and Opera 9.6+

Is there a JavaScript / jQuery DOM change listener?

For a long time, DOM3 mutation events were the best available solution, but they have been deprecated for performance reasons. DOM4 Mutation Observers are the replacement for deprecated DOM3 mutation events. They are currently implemented in modern browsers as MutationObserver (or as the vendor-prefixed WebKitMutationObserver in old versions of Chrome):

MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

var observer = new MutationObserver(function(mutations, observer) {
// fired when a mutation occurs
console.log(mutations, observer);
// ...
});

// define what element should be observed by the observer
// and what types of mutations trigger the callback
observer.observe(document, {
subtree: true,
attributes: true
//...
});

This example listens for DOM changes on document and its entire subtree, and it will fire on changes to element attributes as well as structural changes. The draft spec has a full list of valid mutation listener properties:

childList

  • Set to true if mutations to target's children are to be observed.

attributes

  • Set to true if mutations to target's attributes are to be observed.

characterData

  • Set to true if mutations to target's data are to be observed.

subtree

  • Set to true if mutations to not just target, but also target's descendants are to be observed.

attributeOldValue

  • Set to true if attributes is set to true and target's attribute value before the mutation needs to be recorded.

characterDataOldValue

  • Set to true if characterData is set to true and target's data before the mutation needs to be recorded.

attributeFilter

  • Set to a list of attribute local names (without namespace) if not all attribute mutations need to be observed.

(This list is current as of April 2014; you may check the specification for any changes.)

Is possible to detect a change in the DOM and make it not happen?

Watching for changes:

What you are looking for is a MutationObserver.

Example from MDN

// select the target node
var target = document.querySelector('#some-id');

// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
console.log(mutation.type);
});
});

// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true };

// pass in the target node, as well as the observer options
observer.observe(target, config);

They work on recent versionf of Chrome so you shouldn't have any problem using them in an extension.

As for rolling it back, I suspect you'd have to roll it back yourself.

Here is a strategy for rolling back:

  1. Clone the node you're watching using Node.cloneNode(true) (the parameter indicates a deep clone)
  2. Watch the node with a mutation observer.
  3. Call Node.replaceChild on it from its parent when it changes.

While this is not the most efficient approach it is the simplest and is simple enough to implement. A more drastic but perhaps faster approach would be reverting every mutation yourself using the returned mutation array.

Preventing updates:

If you just want to prevent other code from touching it, there is a simpler way.

  1. Clone the node using Node.cloneNode(true) (the parameter indicates a deep clone).
  2. Wait until you're sure the external code calling it has obtained its reference to it.
  3. Call Node.replaceChild on it from its parent, the external code now is holding a reference to a node that is not in the document and changes will not reflect in the presentation.
  4. Additionally, you might want to change its ID, class name, and other 'identifying' information so the external code won't catch it if it selects it lazily from the DOM.


Related Topics



Leave a reply



Submit