It turns out that document fragments do not implement the
getElementsByTagName()
method. But they do implement the Selectors
API methods: querySelector/querySelectorAll.
Nobody cares about this except for people that write JavaScript selector engines. If you are one of those people then keep reading.
I can code around the absence of a lot of DOM methods but
getElementsByTagName() seemed impossible to do without. So I had to fake it!
JavaScript performance
JavaScript is a nice enough scripting language but it is still a scripting language.
For high performance loops you should try to avoid the following:
try/catchblocks- function/method calls (even on native objects)
- accessing DOM properties (or anything that involves calling a complex getter/setter)
Please note that this is an ordered list.
The code
With all that in mind I wrote this simple implementation of getElementsByTagName()
which I hope is fast enough for those edge cases where users execute DOM queries on document fragments:
function getElementsByTagName(node, tagName) {
var elements = [], i = 0, anyTag = tagName === "*", next = node.firstChild;
while ((node = next)) {
if (anyTag ? node.nodeType === 1 : node.nodeName === tagName) elements[i++] = node;
next = node.firstChild || node.nextSibling;
while (!next && (node = node.parentNode)) next = node.nextSibling;
}
return elements;
};
Update: I’ve amended the original code based on the comments below.
Some things to consider:
- Document fragments cannot have a parent node.
- This method will only be used to query document fragments, so you can assume that the context node will not be an element.
Comment: #1.
You can probably optimize a bit more by avoiding double `node.parentNode` access. E.g.:
function getElementsByTagName(node, tagName) { var elements = [], i = 0, p; while (node) { if (tagName == "*" ? node.nodeType == 1 : node.nodeName == tagName) elements[i++] = node; node = node.firstChild || node.nextSibling || ((p = node.parentNode) && p.nextSibling); } return elements; };Comment: #2.
NICE!
a=document.createDocumentFragment(); b=document.createElement("div"); b.innerHTML="<div></div><div></div>" a.appendChild(b); alert(getElementsByTagName(a,"DIV"))//3Comment: #3.
I guess this completely overlooks the idea that the NodeList is supposed to be “live”, but what can you expect for a JS shim… I like the way you did the traversal there!
Comment: #4.
Note that IE does support getElementsByTagName on document fragments. Assuming that it performs well, I check if the built-in function exists and use it if it does, else use my fallback (which wasn’t nearly as slick as yours).
Comment: #5.
Might still need some work:
a=document.createDocumentFragment(); b=document.createElement("div"); b.innerHTML=" ";
a.appendChild(b);
alert(getElementsByTagName(a,"DIV").length); //2, but should be 3
Para
Comment: #6.
There’s a logic error in the line:
It’ll exit the loop when node’s parentNode doesn’t have a nextSibling, though there are plenty of cases where this doesn’t mean all nodes have been visited. E.g. it’ll miss <d> in <a><b><c></c></b></a><d></d>.
To fix it, you’d need to replace that line with something like:
if (node.firstChild) { node = node.firstChild; } else { while (node = node.parentNode) { if (node.nextSibling) { node = node.nextSibling; break; } } }Comment: #7.
If you’re focused on optimization, why use equivalence testing (==) when you could use equality testing (===)? A quick test in Firefox 3.5 shows that “*” == “*” takes about 40% longer than “*” === “*”. Might not be much compared to the DOM accessing, but hey, it’s a free optimization, and you’re doing three of them!
Comment: #8.
Equally, if performance is the goal, why have an if statement that checks something unchanging out of the loop? Certainly it doubles the size of the code, but it removes a needless comparison.
Comment: #9.
@Scott : you may wish to stop in your parent node loop at the context node supplied to the function.
@Jakob/Jim : I have also suggested memoising the the “*”
function getElementsByTagName(node, tagName) { var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next; do { if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node; if (!(next = node.firstChild || node.nextSibling)) { while((node = node.parentNode) && node !== root && !(next = node.nextSibling)) {} } } while(node = next); return elements; }Comment: #10.
@Jakob
Last time I tried testing the performance of ‘==’ vs. ‘===’ I didn’t notice any difference. In theory it should be faster because the interpreter has less to think about, but that would depend on how the interpreter has been coded I guess.
Comment: #11.
Forgot the case where the supplied node is empty.
function getElementsByTagName(node, tagName) { var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next; if (!node.firstChild) { elements[i] = node; return elements;} do { if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node; while(!(next = node.firstChild) && !(next = node.nextSibling) && (node = node.parentNode) && node !== root && !(next = node.nextSibling)) {} } while(node = next); return elements; }Comment: #12.
Final attempt (forgetting that the context node should not be returned):-
function getElementsByTagName(node, tagName) { var elements = [], i = 0, anyTag = (tagName === "*"), root = node, next = node.firstChild; while (node = next) { if (node.nodeType === 1 && (anyTag || node.nodeName === tagName)) elements[i++] = node; if (next = node.firstChild || node.nextSibling) continue; while((node = node.parentNode) && node !== root && !(next = node.nextSibling)) { /* empty */} } return elements; }Comment: #13.
@Jakob and Pete: I suppose in using “===” we would need to be wary of the unlikely case : “a” !== (new String(”a”))
Comment: #14.
Thanks for the feedback everybody, particularly Julian. I’ve updated my code based on your suggestions.
Comment: #15.
I was doing that this way, but your implementation is faster. Thanks!
function getElementsByTagName( node, tagName ){ };Comment: #16.
Another thing to avoid for performance is deleting properties (in all the latest engines, that’s something that is really expensive, due to the representation of objects internally).
Comment: #17.
It could be a nice NodeList with this method :
elements.item = function (i) { return this[i]; };Comment: #18.
Comment: #19.
Hi Dean, any bench against this, which is live as well?
function getElementsByTagName(node, nodeName){ var tmp = document.createElement("tmp"); return tmp.appendChild(node) && tmp.getElementsByTagName(nodeName); };Comment: #20.
@Andrea (WebReflection)
The node list doesn’t need to be live. This is an internal method just for use by selector engines.
Your solution is a good idea but unfortunately it does not work because of a behaviour unique to document fragments. Once you append a document fragment to an element, the fragment disappears and the fragment’s children become direct children of the element that the fragment is appended to. I hope I explained that properly.
Comment: #21.
still me, I kinda realize how silly was my snippet the instant after I pressed submit
so … what about this? Still talking about performances, I do like your solution.
function getElementsByTagName(node, nodeName){ for(var tmp = document.createElement("tmp"), childNodes = tmp.appendChild(node) && tmp.getElementsByTagName(nodeName), i = 0, length = childNodes.length, result = []; i < length; ++i ) result[i] = childNodes[i] ; childNodes = tmp.childNodes; length = childNodes.length; while(length) node.appendChild(childNodes[--length]) ; return tmp = result; };Comment: #22.
I need some rest …
function getElementsByTagName(node, nodeName){ for(var tmp = document.createElement("tmp"), childNodes = tmp.appendChild(node) && tmp.getElementsByTagName(nodeName), i = 0, length = childNodes.length, result = []; i < length; ++i ) result[i] = childNodes[i] ; childNodes = tmp.childNodes; length = childNodes.length; while(length--) node.appendChild(childNodes[0]) ; return tmp = result; };could be probably smaller via while …
while(childNodes = tmp.firstChild) node.appendChild(childNodes) ;not sure which one will perform better though … I’ll try some bench later
Comment: #23.