Wednesday, October 14, 2009

DOM Node Proxy

This is just a quick post from home sweet home.
A common DOM related problem is to create an association between a node and a generic object. The most dirty, memory leaks prone, and obtrusive way to perform this task is this one:

document.body.obj = {
prop:"value",
otherProp:function(){}
};

Above snippet is a bad practice for different reasons.
  1. obtrusive, it's assuming that no other libraries will use "obj" property name to perform an analogue task
  2. dirty, if we associate a primitive value Internet Explorer will expose it in the node string representation
  3. memory leaks, if the object points something "live", another node, or a HTMLCollection, the generic node will never be collected by the garbage


Alternatives

Specially to avoid last problem, the memory consumption, it's a good practice to store an index, rather than an object. To make things less obtrusive and get rid of conflicts, we usually create a "unique id".

// the array with all objects
var stack = [];

// the unobtrusive property name
var expando = "prefix" + new Date().getTime();

// the obejct to relate
var o = {};

stack.push(o);

// the relation via index (last object)
document.body[expando] = stack.length - 1;

As I have already linked and explained, this technique is still dirty because Internet Explorer will show the unique id via outerHTML or generic node representation.

Strategies

jQuery, and many others, create an association for each manipulated dom node. This could consume RAM without a reason since there could be a lot of nodes with an associated object that will never be used.
Next version of jQuery, right now in alpha stage, understood this point changing the object association logic. I have not read how yet, but I would like to write something I've been used for a while, a sort of proxy object created for DOM nodes and object relations.

DOM Node Proxy

var proxy = (function(){
// another (C) WebReflection silly idea
var expando = "@".concat(+new Date, Math.random()),
stack = []
;
return function proxy(){
return stack[this[expando]] || stack[
this[expando] = new Number(stack.push({}) - 1)
];
};
})();

Above snippet uses almost all strategies I know to avoid obtrusive property, dirty layout, and direct object assignment (index strategy).
To better understand what exactly is above function I have commented each part of it:

var proxy = (function(){

// another (C) WebReflection silly idea

// one function to associate as proxy
// Being assignment a direct one
// standard browsers won't modify
// attributes while being proxy
// an object (not primitive value)
// IE won't expose it in node string
// representation (e.g. outerHTML)
function proxy(){

// a proxy call costs only once:
// the first time it's called
// Other calls will return the object
// This is to avoid objects association
// even if these are not necessary
return stack[this[expando]] || stack[

// the index is the last one in the
// private stack Array. To avoid
// leaks we don't associate directly
// an object but simply an integer.
// If we directly associate
// a primitive value, IE will expose
// is in the dom string representation
// (e.g. outerHTML)
// To avoid this we can just assign
// a Number instance, rather than
// a primitive "number"
this[expando] = new Number(

// push returns the new length
// we need last inserted object
// index to relate the object
stack.push({}) - 1
)
];
};

var
// private unique expando with
// an invalid char as prefix
// in order to make attr name
// easy to recognize in a possible
// IE attributes loop
expando = "@".concat(
+new Date,
Math.random()
),

// list of associated object
stack = []
;

// ready to go!
return proxy;

})();

Is it clear enough? This is a simple usage example (please reado NOTEs):

onload = function(){

// associate a proxy
// NOTE: this is still obtrusive
// the property name should be a unique id
// or it should have library prefix
// otherwise we could have conflicts
document.body.proxy = proxy;
// aka: node[expando] = proxy;

// retrieve the proxy object
var p = document.body.proxy();

// test proxy: true
alert(p === document.body.proxy());

// test clean body string representation
alert(document.documentElement.innerHTML);

// find proxy created property
for(var k in document.body){
if(k.charAt(0) === "@")
alert([k, document.body[k]])
;
}
};


As Summary

This is more a proof of concept but I hope showed code will help us to replicate the behavior. The main missed part is the internal stack management: how can I clean the stack index when I don't need the node anymore? All we need is an extra in-proxy-scope function or a specific associated instance rather than a raw object.
In ew words there are no best strategies for this second problem, it just depends what we need.
From a logical point of view, if we give indirect access to that stack, exposing its length or via functions able to modify it, stack safety could be compromised. What I could suggest is something like:
var proxy = (function(){
// (C) WebReflection - Mit Style License
function proxy(){
return stack[this[expando]] || stack[
this[expando] = new Number(stack.push(new $proxy) - 1)
];
};
function $proxy(){
this._index = stack.length;
};
$proxy.prototype.destroy = function destroy(){
delete stack[this._index];
};
var expando = "@".concat(+new Date, Math.random()),
stack = []
;
return proxy;
})();
where the stack is manipulated indirectly while nothing is publicly exposed.

No comments:

Post a Comment