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

Wednesday, December 17, 2008

outerHTML for almost every browser ... if you need it ...

We all have to deal with memory leaks problem and apparently a good way to be sure that an HTMLElement has been removed from its "flow" is the outerHTML assignemnt (thanks Ariel for the suggestion)

element.outerHTML = "";
element = null;

If the element is not a document.body or another body parent node, Internet Explorer will "extract" that element from its context, if any, and if there are no other references for that element the null assignment will, theoretically, complete the opera, hopefully solving memory leaks problem for that node as well ...


To remove ... but to replace too ...


Every library has a "swap" or replace method to quickly change an element, but even if we all know that innerHTML is the fastest way to insert content, few libraries use outerHTML to replace nodes, that as far as I know, should be "that fast" in IE as innerHTML is:

// traditional swap, the DOM way
function swap(oldNode, newNode){
var parentNode = oldNode.parentNode;
parentNode.insertBefore(newNode, oldNode);
parentNode.removeChild(oldNode);
};

// spaghetti swap, the outerHTML way
function swap(oldNode, newNode){
oldNode.outerHTML = newNode.outerHTML;
};



outerHTML ... both get and set



try{
HTMLElement.prototype.__defineGetter__.length;
(function(body, removeChild){
HTMLElement.prototype.__defineGetter__(
"outerHTML",
function(){
var self = body.appendChild(this.cloneNode(true)),
outerHTML = body.innerHTML;
body.removeChild(self);
return outerHTML;
}
);
HTMLElement.prototype.__defineSetter__(
"outerHTML",
function(String){
if(!String)
removeChild(this);
else if(this.parentNode){
body.innerHTML = String;
while(body.firstChild)
this.parentNode.insertBefore(body.firstChild, this);
removeChild(this);
body.innerHTML = "";
};
}
);
})(
document.createElement("body"),
function(HTMLElement){if(HTMLElement.parentNode)HTMLElement.parentNode.removeChild(HTMLElement);}
);
}catch(e){};



Conclusion


pro and cons are the same of innerHTML and the reference is lost as is for Internet Explorer, whenever we decide to use this dirty approach to remove or change elements in the DOM. A last example?

function change(strong){
strong.outerHTML = "not strong anymore";
strong = null;
};
onload = function(){
document.body.innerHTML = "click";
};

8 comments:

Már said...

I'm pretty sure both of these will fail on loads of edge-cases - like table innards.

Andrea Giammarchi said...

can you provide me an example, please?

kentaromiura said...

mmm, bigsbò, outerXML have a side issue, if you got a node and then you replace the node using outerXML, the reference to that node remain, even if it doesn't exist anymore
(is pretty logical btw)
That's why here: http://mykenta.blogspot.com/2006/07/standardise-ie-setattribute-part-2.html I return the new node in the case "name"

kangax said...

Could you provide a testcase which proves that simply null'ing references is not enough?

Andrea Giammarchi said...

kangax, null the reference is ok but if the element is in the DOM it is pointless as well while to remove the node you can choose the DOM way and then null the reference or simply use outerHTML and then null it.

In any case, this is an outerHTML implementation for other browsers, I know it could sound useless but sometimes we have to develop mainly for IE and then for other browsers ... this stuff is for those cases :-)

kenta, I said limits are the same of innerHTML, you loose references and you have to be acreful with these "trick" ;-)

kangax said...

@Andrea,
Ok. It just sounded like `null`ing by itself is not enough to break the reference and that additional `outerHTML` "clearing" is needed : ) Thanks for clarification; Glad to hear it's not the case.

Juanito said...

It looks like the getter for outerHTML won't work because it's appending the temp node to the body and then getting the innerHTML of the body which could contain other nodes. The best solution would be to have an invisible absolutely positioned node attached to the body and you could add the cloned node in there!

However, I love the idea. I had never thought of implementing IE specific methods/properties by extending HTML element.

About Már's comment. This approach also won't workd if you are trying to get the outerHtml of a tr since you can't append a tr to antyhing but a table (or a td to anything but a tr). The solution for that would be to add a switch on the type of node and use a table (or td) as the parent node. You could also do it by overriding the outerHTML property of trs and tds

Juanito said...

I don't know why you think that this would be less leak prone than say:

node.parentNode.removeChild(node)

In both cases (your approach and the traditional approach I mentioned), you should still clear out event handlers and references to js objects when you remove from the DOM.

From my research, memory leaks happen in IE when you remove nodes from the DOM and those nodes had cyclical references (through closures or expando properties). This happens because IE's current strategy is to walk the dom all null out all references to avoid cyclical references between the DOM and JS. However, this approach does not null out objects that were removed from the DOM (since walking the tree won't get at those elements).

I currently use Ext-js which spawns a thread that cleans up all orphaned Element objects periodically avoiding this altogether. Therefore, for Ext-js users, using your technique will work quite well.

Something to think about:

If your node contains form elements, cloneNode (in IE at least) will clone the nodes as they were initially on the page. So if you've checked a checkbox, that will not be reflected in the outerHTML. However, innerHTML also returns the html as initially loaded, so it may be ok!