My JavaScript book is out! Don't miss the opportunity to upgrade your beginner or average dev skills.

Monday, September 14, 2009

LiveMonitor - Asynchronous Property Monitor

Today I would like to introduce you a quite uncommon JavaScript trick, a trapped Live Object or, generally speaking, a lightweight monitor able to understand when a generic property has been changed.

About Live Objects

A live object could be described as a particular object able to change without our interaction. The most common live object example is this:

// this is the most common live object
// the HTMLCollection
var divs = document.getElementsByTagName("div");
divs.length; // let's say 4

// let's add another div inside a generic node
document.body.appendChild(
document.createElement("div")
);

divs.length; // 5!

In few words DOM searches are dynamic, which is the reason almost every selector library needs to transform the current result into a static Array.

About LiveMonitor

Specially suited for live objects, LiveMonitor is a function which aim is to notify us when the specified property change:

// LiveMonitor example
var lm = new LiveMonitor(
// the entire list of elements trapped
document.getElementsByTagName("*"),
// the property to monitor
"length"
);

// add one or more notifier
lm.onchange(function(collection){
// this, will be the lm object
this.value; // will be collection.length
collection.length; // is the new length

alert([
"All nodes collection contained ",
this.value,
" nodes but now there are ",
collection.length
].join("\n"));
});

The example is quite silly, but the concept is that as soon as the monitor will check the "length" property, in this case in the one of the trapped collection, it will fire the onchange event, notifying us that somebody did something somewhere, and this something changed our monitored property.

LiveMonitor Code


var LiveMonitor = (function(){

/** LiveMonitor :: Asynchronous Property Monitor
* @author Andrea Giammarchi
* @license Mit Style
* @blog http://WebReflection.blogspot.com/
*/

function LiveMonitor(object, property){
this.value = object[property];
this._property = property;
this._object = object;
this._event = [];
this._i = 0;
};

LiveMonitor.prototype.onchange = function onchange(callback){
this._event.push(callback);
if(this._i === 0){
var _property = this._property,
_object = this._object,
_event = this._event,
self = this
;
this._i = setTimeout(function _i(){
if(self.value !== _object[_property]){
for(var i = 0, length = _event.length; i < length; ++i){
if(_event[i].call(self, _object) === false)
break
;
};
self.value = _object[_property];
};
if(0 < self._i)
self._i = setTimeout(_i, 15)
;
}, 15);
};
};

LiveMonitor.prototype.offchange = function offchange(callback){
for(var _event = this._event, i = 0, length = _event.length; i < length; ++i){
if(_event[i] === callback)
_event.splice(i, 1)
;
};
if(_event.length === 0){
clearTimeout(this._i);
this._i = 0;
};
};

LiveMonitor.prototype.clear = function clear(){
this._event = [];
this.offchange(null);
this.value = this._object[this._property];
};

return LiveMonitor;

})();


Not Only DOM Searches

LiveMonitor could be actually used as asynchronous notifier for any kind of variable.
Let's say we have an environment able to load runtime scripts but we cannot change loaded scripts (external source inclusion).
At some point we press the "load jQuery" button and we would like to be able to be notiied when it is available ...

if(!window.jQuery){

// create a LiveMonitor instance over
// jQuery property
var jqlm = new LiveMonitor(window, "jQuery");

// add onchange event
jqlm.onchange(function(window){

// remove this event
this.offchange(arguments.callee);
// remove jqlm as well, it is not useful
// anymore for this example

// use jQuery, it's here for sure!
$("body").html("Hello LiveMonitor!");
});

// load external script
loadScript("http://external/jQuery.js");
};


Conclusion

With a cross-browser, portable, and lightweight function, we can use notifications without effort over objects, HTMLCollections, Arrays, or everything else we would like to monitor. Let's say this is a sort of asynchronous cross browser Object.prototype.watch, but this time without alchemy.

7 comments:

Malte said...

Hey,

in modern browsers you could listen for DOM mutation events to only look for property changes when neccessary:
http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mutationevents

Andrea Giammarchi said...

hey, i still deal with IE6 on daily basis :D

jokes a part, that is why I said it is like a cross browser watch, also mutation events could be truly greedy. As example, if you use LiveMonitor with getElementsByTagName("*") the event will be fired once about every 15 ms, if you use a DOMNodeInserted in the document, the entire application will slow down and the event fired for each innerHTML or inserted node.

So let's say this is an alternative ;)

Andrea Giammarchi said...

P.S. also LiveMonitor is not only for DOM, it could be used with any kind of gloibal or nested variable too :)

Andrea Giammarchi said...

... plus live objects won't be supported ...

Keanu said...

A good use would be in a selector engine like Sizzle, and only busting the cached results when the DOM is changed in any way.

- Karmagination

Andrea Giammarchi said...

Unfortunately Sizzle needs to be synchronous due to constant add/remove/replace DOM manipulation via jQuery or whatever other library based on Sizzle.

It used DOMTreeModified event, initially, but for general DOM slowdown problems John had to remove it. This trick is less greedy but again, async ... let's see if anybody will use it for other purposes

Unknown said...

If there is a 15ms timer cost for each property to monitor, I wonder what will happen when we have a few hundred objects each having several properties being monitored at any given time?

Any tests on these Andrea?