Why Document.querySelector is more efficient than Element.querySelector - javascript

I did a test with few iterations to test efficiency of Document.querySelector and Element.querySelector.
Markup:
<form>
<input type="text" />
</form>
Script:
Querying with Document.querySelector
begin = performance.now();
var
i = 0,
iterations = 999999;
for ( i; i < iterations; i++ )
{
element = document.querySelector('[type="text"]');
}
end = performance.now();
firstResult = end - begin;
Querying with Element.querySelector
begin = performance.now();
var
i = 0,
iterations = 999999,
form = document.querySelector('form');
for ( i; i < iterations; i++ )
{
element = form.querySelector('[type="text"]');
}
end = performance.now();
secondResult = end - begin;
Log:
console.log( firstResult ); // 703.7450000001118
console.log( secondResult ); // 1088.3349999999627
The log is amazing for me because i think that Element.querySelector query only on nodes that is a descendant of the element and Document.querySelector query on all nodes of current document, right?
Why get this result?

From my comment above, the selector takes into account the entire document, then filters the items to check if they are descendants of the target. So it's likely that it still needs to scan the entire DOM tree like document.querySelector would need to do.
There is a discussion of the issue (that is still the current behaviour) here. You'll see in the code sample below the span is included as a result, because it can't just query items below foo in isolation.
Fiddle
Code:
document.body.innerHTML = '<div><p id="foo"><span></span></p></div>';
var foo = document.getElementById('foo');
alert( foo.querySelectorAll('div span').length);

It seems obvious to me that the result should be null, as the context is outside the element the search is began from.
Things are simple when you make them simple.... Don't you are fixing the "context", how can you think the "context" is "outside" the "context" you stated... In the example from Evan is really curious to get any result, as there are no "div" in the foo node where the selector is executed... but the answer is "I've found a span descendant of a div... but you'll not find the div there"

Related

Need help optimizing script that hides table rows

I made this Greasemonkey script:
var maxpi = 250;
var p1 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[1]/td[11]";
var p2 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[2]/td[11]";
..
var p25 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[25]/td[11]";
var r1 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[1]";
var r2 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[2]";
..
var r25 = "/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[25]";
var xpathPI1 = document.evaluate(p1, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
..
var xpathPI25 = document.evaluate(p25, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
var xpathrow1 = document.evaluate(r1, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
..
var xpathrow25 = document.evaluate(r25, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
if (xpathPI1.singleNodeValue.textContent >maxpi ){
xpathrow1.singleNodeValue.style.display='none';}
..
if (xpathPI25.singleNodeValue.textContent >maxpi ){
xpathrow25.singleNodeValue.style.display='none';}
Basically, it checks a table row's 11th field and if its contents > than 250 it hides the row.
With my limited javascript knowledge took quite some time get this working.
The problem is that I have to rewrite every single line if I want to check-hide another row.
I want to make it more usable so I can use it on similar tables without rewriting the whole thing.
Maybe I need to use a different XPath type or use some kind of changing variable?
Of course, there are more ways to improve your script.
Firstly, you need to thoroughly think through WHAT exactly you want to look for. Is is every row and column? Is it rows/columns with some text, class, any other attribute? You can even select only those nodes that have their text value greater than your maxpi!
Read something about XPath, the possibly best resource is the official one.
Some random thoughts on what could be useful regarding XPath:
//table//tr[5]/td[2] ... the double slash is the deal here
//table//tr/td[number(text()) > 250] ... the number() and text() functions
When talking about JavaScript, that would be a little tougher, because there are so many things you could use!
Just for starters - you can create dynamically changing xpath expressions by String concatenation and For loop, like this:
for (var i = 1; i <= maxNumberOfRows; i++) {
var p1 = "//table/tbody/tr[" + i + "]";
// more work goes here...
}
Also, you could use arrays to store multiple nodes returned by your XPath expressions and work on them all with just a single command.
For more JavaScript, I would recommend the first chapters of some JavaScript tutorial, that will boost your productivity by a lot.
Use a loop and functions. Here's one way:
hideRowsWithLargeCellValue (
"/html/body/div/div[2]/div/div[2]/table[2]/tbody/tr[",
25,
"]/td[11]",
250
);
function hideRowsWithLargeCellValue (xpathPre, maxRows, xpathPost, maxpi) {
for (var J = maxRows; J >= 1; --J) {
var srchRez = document.evaluate (
xpathPre + J + xpathPost,
document,
null,
XPathResult.FIRST_ORDERED_NODE_TYPE,
null
);
if (srchRez.singleNodeValue && srchRez.singleNodeValue.textContent > maxpi) {
var rowToHide = srchRez.singleNodeValue.parentNode;
rowToHide.style.display='none';
}
}
}
Then read "Dont Repeat Yourself" (sic).

infinite-loop via prepend element in DOM

Not Looking for a Use Framework XXX Answer
This question is not intended for finding a practical solution via a framework. Answering with use framework XXX, or this is so easy in framework XXX, or why not use this framework XXX??? doesn't answer the question.
I have a function meant to run after a page has been loaded: performShim. This function iterates over all elements in the DOM that are span tags, checks if they have a className of shim and if so, calls shim passing to it a reference of the matched element.
My goal was to prepend another span that contains an iframe to the element that is passed to shim.
With the code I wrote so far, I am able to append to the element's parent just fine. However, if I comment out the append line and instead try the prepend line the browser hangs in presumably an infinite-loop.
It's not readily obvious to me why this is the case.
function shim( element ) {
var iframe = document.createElement('iframe');
iframe.setAttribute( 'frameborder', '0' );
iframe.setAttribute( 'scrolling', 'no' );
iframe.setAttribute( 'align', 'bottom' );
iframe.setAttribute( 'marginheight', '0' );
iframe.setAttribute( 'marginwidth', '0' );
iframe.setAttribute( 'src', "javascript:'';" );
var span = document.createElement('span');
span.appendChild(iframe);
//element.parentNode.insertBefore(span,element); //causes infinite loop?
element.parentNode.appendChild(span); //this line works OK
var els = element.style;
var originalVisibility = els.visibility;
var originalPosition = els.position;
var originalDisplay = els.display;
els.visibility = 'hidden';
els.position = 'absolute';
els.display = 'inline';
var width = element.offsetWidth;
var height = element.offsetHeight;
els.display = originalDisplay;
els.position = originalPosition;
els.visibility = originalVisibility;
iframe.style.width = (width-6) + 'px';
iframe.style.height = (height-6) + 'px';
}
function performShim() {
var children = document.getElementsByTagName("span");
for( var i = 0; i < children.length; i++ ) {
if( children[i].className == "shim" ) {
shim(children[i]);
}
}
}
A NodeList (such as the one returned by document.getElementsByTagName) is typically a live list -- changes you make to the DOM show up in it as well. So each time you add a span before the current one, you're extending the list by one element and moving the current element over by one, and the next iteration puts you right back at the node you just finished.
You have a couple of easy workarounds for that...
Bump the counter when you add a node. (Ugly, and if you ever end up adding something instead of a span, you'll end up skipping nodes and it won't be obvious why.)
Copy the list to an array and iterate over the array. You could do this with something like
children = [].slice.call(children, 0); (more common) or
children = Array.apply(window, children);.
Use document.querySelectorAll, which returns you a NodeList that's not live. (And even if it were live, in this case you could select 'span.shim' and the inserted spans wouldn't show up in it anyway.)
Iterate backwards (from children.length - 1 to 0).

how to get all parent nodes of given element in pure javascript?

I mean an array of them. That is a chain from top HTML to destination element including the element itself.
for example for element <A> it would be:
[HTML, BODY, DIV, DIV, P, SPAN, A]
A little shorter (and safer, since target may not be found):
var a = document.getElementById("target");
var els = [];
while (a) {
els.unshift(a);
a = a.parentNode;
}
You can try something like:
var nodes = [];
var element = document.getElementById('yourelement');
nodes.push(element);
while(element.parentNode) {
nodes.unshift(element.parentNode);
element = element.parentNode;
}
I like this method:
[...(function*(e){do { yield e; } while (e = e.parentNode);})($0)]
... where $0 is your element.
An upside of this method is that it can be used as a value in expressions.
To get an array without the target element:
[...(function*(e){while (e = e.parentNode) { yield e; }})($0)]
You can walk the chain of element.parentNodes until you reach an falsey value, appending to an array as you go:
const getParents = el => {
for (var parents = []; el; el = el.parentNode) {
parents.push(el);
}
return parents;
};
const el = document.querySelector("b");
console.log(getParents(el).reverse().map(e => e.nodeName));
<div><p><span><b>Foo</b></span></div>
Note that reversing is done in the caller because it's not essential to the lineage algorithm. Mapping to e.nodeName is purely for presentation and also non-essential.
Note that this approach means you'll wind up with the document element as the last element in the chain. If you don't want that, you can add && el !== document to the loop stopping condition.
The overall time complexity of the code above is linear and reverse() is in-place, so it doesn't require an extra allocation. unshift in a loop, as some of the other answers recommend, is quadratic and may harm scalability on uncommonly-deep DOM trees in exchange for a negligible gain in elegance.
Another alternative (based on this):
for(var e = document.getElementById("target"),p = [];e && e !== document;e = e.parentNode)
p.push(e);
I believe this will likely be the most performant in the long run in the most scenarios if you are making frequent usage of this function. The reason for why t will be more performant is because it initially checks to see what kind of depths of ancestry it might encounter. Also, instead of creating a new array every time you call it, this function will instead efficiently reuse the same array, and slice it which is very optimized in some browsers. However, since there is no really efficient way I know of to check the maximum depth, I am left with a less efficient query-selector check.
// !IMPORTANT! When moving this coding snippet over to your production code,
// do not run the following depthtest more than once, it is not very performant
var kCurSelector="*|*", curDepth=3;
while (document.body.querySelector(kCurSelector += '>*|*')) curDepth++;
curDepth = Math.pow(2, Math.ceil(Math.log2(startDepth))),
var parentsTMP = new Array(curDepth);
function getAllParentNodes(Ele){
var curPos = curDepth;
if (Ele instanceof Node)
while (Ele !== document){
if (curPos === 0){
curPos += curDepth;
parentsTMP.length <<= 1;
parentsTMP.copyWithin(curDepth, 0, curDepth);
curDepth <<= 1;
}
parentsTMP[--curPos] = Ele;
Ele = Ele.parentNode;
}
return retArray.slice(curPos)
}
The browser compatibility for the above function is that it will work in Edge, but not in IE. If you want IE support, then you will need a Array.prototype.copyWithin polyfill.
get all parent nodes of child in javascript array
let selectedTxtElement = document.getElementById("target");
let els = [];
while (selectedTxtElement) {
els.unshift(selectedTxtElement);
selectedTxtElement = selectedTxtElement.parentNode;
}
know more

Is there any way to find an element in a documentFragment?

var oFra = document.createDocumentFragment();
// oFra.[add elements];
document.createElement("div").id="myId";
oFra.getElementById("myId"); //not in FF
How can I get "myId" before attaching fragment to document?
All of these answers are rather old, from back when querySelectorAll and querySelector were not widely available. It should be noted that these two functions which accept CSS selectors as parameters do work on DocumentFragments in modern browsers, and should be the preferred way of dealing with the situation in the question. The alternate solutions presented in some of the answers would be a good approach for legacy browsers which did not support querySelectorAll or querySelector.
Here is an example usage:
var df = document.createDocumentFragment();
var div = document.createElement('div');
div.id = 'foo';
df.appendChild(div);
var result = df.querySelector('#foo'); // result contains the div element
A good implementation should first use object detection to see if the browser supports this. For instance:
function getElementByIdInFragment(fragment, id) {
if (fragment.querySelector) {
return fragment.querySelector('#' + id);
} else {
// your custom implementation here
}
}
No. The DocumentFragment API is minimal to say the least: it defines no properties or methods, meaning that it only supports the properties and methods defined in the Node API. As methods such as getElementById are defined in the Document API, they cannot be used with a DocumentFragment.
NickFitz is right, DocumentFragment doesn't have the API you expect from Document or Element, in the standard or in browsers (which is a shame; it would be really handy to be able to set a fragment's innerHTML.
Even frameworks don't help you here, as they tend to require Nodes be in the document, or otherwise use methods on the context node that don't exist on fragments. You'd probably have to write your own, eg.:
function Node_getElementById(node, id) {
for (var i= 0; i<node.childNodes.length; i++) {
var child= node.childNodes[i];
if (child.nodeType!==1) // ELEMENT_NODE
continue;
if (child.id===id)
return child;
child= Node_getElementById(child, id);
if (child!==null)
return child;
}
return null;
}
It would almost certainly be better to keep track of references as you go along than to rely on a naïve, poorly-performing function like the above.
var frag= document.createDocumentFragment();
var mydiv= document.createElement("div");
mydiv.id= 'myId';
frag.appendChild(mydiv);
// keep reference to mydiv
What about:
var oFra = document.createDocumentFragment();
var myDiv = document.createElement("div");
myDiv.id="myId";
oFra.appendChild(myDiv);
oFra.getElementById("myId"); //not in FF
Unless you've added the the created div to your document fragment I'm not sure why getElementById would find it?
--edit
If you're willing to roll your own getElementById function then you ought to be able to get the reference you're after, because this code works:
var oFra = document.createDocumentFragment();
var myDiv = document.createElement("div");
myDiv.id = "myId";
oFra.appendChild(myDiv);
if (oFra.hasChildNodes()) {
var i=0;
var myEl;
var children = oFra.childNodes;
for (var i = 0; i < children.length; i++) {
if (children[i].id == "myId") {
myEl = children[i];
}
}
}
window.alert(myEl.id);
Using jQuery:
// Create DocumentFragment
var fragment = document.createDocumentFragment(),
container = document.createElement('div');
container.textContent = 'A div full of text!';
container.setAttribute('id', 'my-div-1');
container.setAttribute('class', 'a-div-class');
fragment.appendChild(container);
// Query container's class when given ID
var div = $('<div></div>').html(fragment);
console.log(div.find('#my-div-1').attr('class'));
jsFiddle: http://jsfiddle.net/CCkFs/
You have the overhead of creating the div with jQuery, though. A little hacky, but it works.
The best way by far to find out what you can and can't do with a DocumentFragment is to examine its prototype:
const newFrag = document.createDocumentFragment();
const protNewFrag = Object.getPrototypeOf( newFrag );
console.log( '£ protNewFrag:' );
console.log( protNewFrag );
I get
DocumentFragmentPrototype { getElementById: getElementById(),
querySelector: querySelector(), querySelectorAll: querySelectorAll(),
prepend: prepend(), append: append(), children: Getter,
firstElementChild: Getter, lastElementChild: Getter,
childElementCount: Getter, 1 more… }
... which tells me I can do things like:
const firstChild = newFrag.children[ 0 ];
PS this won't work:
const firstChild = Object.getPrototypeOf( newFrag ).children[ 0 ];
... you'll be told that "the object doesn't implement the DocumentFragment interface"
An external source, listed below, showed the following code snippet:
var textblock=document.createElement("p")
textblock.setAttribute("id", "george")
textblock.setAttribute("align", "center")
Which displays a different way of setting the object's ID parameter.
Javascript Kit - Document Object Methods
My DOM has a #document-fragment under the element tag.
This is what I am using (using jQuery) , Also I have a use case where I have the HTML DOM in a string -
var texttemplate = $(filecontents).find('template').html();
$(texttemplate).children()
<p>​Super produced One​</p>​,
<appler-one>​</appler-one>,
<p>​Super produced Two​</p>,
<appler-two>​…​</appler-two>]
$(texttemplate).html()
"<p>Super produced One</p>
<appler-one></appler-one>
<p>Super produced Two</p>
<appler-two>
<p>Super produced Three</p>
<appler-three></appler-three>
</appler-two>"
$(texttemplate).find("appler-one")
[<appler-one>​</appler-one>​]

Why this javascript is not working in IE?

I am using the following javascript to dynamically add rows in a table:-
var trObj = document.createElement('tr');
trObj.setAttribute('name', 'dynamicTR');
var tdObjEmpty = document.createElement('td');
tdObjEmpty.setAttribute('colspan', '2');
tdObjEmpty.innerHTML = ' '
trObj.appendChild ( tdObjEmpty );
var tdObj = document.createElement('td');
tdObj.setAttribute('colspan', '15');
tdObj.innerHTML = postingDivObj.innerHTML; // <-- copy the innerHTML
trObj.appendChild ( tdObj );
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
targetElementObj = getNthTr ( parentObj, rowIndex1 - extraTr ); // <-- it will just return the trObject,
if ( targetElementObj ){
parentObj.insertBefore(trObj, targetElementObj.nextSibling )
}else{
//alert ( 'targetElementObj is null' );
}
This is working in FF as well as in IE, [ but, i guess, in case of IE name and colspan attribute is not set using setAttribute. but not sure ] .
Now, when i have to remove all rows which are dynamically created i use:-
dynamicTRObjs = document.getElementsByName('dynamicTR');
if ( dynamicTRObjs ){
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
for ( i = 0 ; i < dynamicTRObjs.length; i++ ){
parentObj.removeChild ( dynamicTRObjs[i] );
extraTr++;
}
}
This code removes all dynamically created TRs. and it works fine in FF, but not in IE.
Also in case of IE dynamicTRObjs.length is always 0,whereas in FF dynamicTRObjs.length it gives correct number of rows. Please tell me what i am missing here.
The HTML4 spec list of attributes lists elements that the name attribute can be set on. Tables and table elements are not on the list. The most obvious option is one of,
Keep references to all TRs you create so you don't have to find them in the DOM
Set a className on your TRs and use selectors to find them
That Firefox uses getElementsByName 'correctly' and IE does not is something others have run into too. I'd just avoid using name here altogether.
http://www.quirksmode.org/dom/w3c_core.html
getElementsByName() is not working well in IE6-8
I would suggest that you some other way of identifying that element if you want cross browser usability.
I know, it's a bit off-topic, but let me give you a small advice on using getElementsByName functionality in a browser. It will not help you to solve the current problem (which is because TR can not have Name attribute ), but it will definitely help you to prevent future problems which you will met.
getElementsByName returns you collection, which always keeps itself up-to-date with the DOM tree. This means, that at the moment when you remove ONE item with removeChild, the SIZE of collection will be decreased. So, if you will removing nodes and keep relying on the length of the collection, not all nodes will be removed.
Check this example of your for loop:
Collection length is 3, Your i var is 0, i < length
You remove child,
collection length is 2, your i var is 1, i < length
you remove child,
collection length is 1 and your i var i 2.
Condition i< length == true that means that for loop will stop, BUT some of the elements will still be presented in the DOM.
Choose any solution you like to fix this, but try to avoid relying on the length of the Collection which is returned by getElementsByTagName.
Good luck
since I'm not the only one to suggest avoidance of low-level DOM manipulation, here's an example: an untested implementation with jquery. not exactly an answer to your question, but a comment would lose the formatting.
var mkTd = function (colspan, html)
{
return $('<td />')
.attr('colspan', colspan)
.html(html)
;
}
var addRow = function (rowNr)
{
var target = $('#approvedDisapprovedTableObj tbody tr:eq('+rowNr+')');
if (!target.length) {
//alert ( 'target is null' );
return;
}
target.after(
$('<tr />')
.addClass('dynamicTR')
.append(mkTd(2, ' ')
.append(mkTd(15, $('#postingDivObj').html()))
);
}
var dropRows = function ()
{
$('.dynamicTR').remove();
}
notice that the expression $('.dynamicTR').remove() achieves the same as your
dynamicTRObjs = document.getElementsByName('dynamicTR');
if ( dynamicTRObjs ){
parentObj = approvedDisapprovedTableObj.getElementsByTagName('tbody')[0];
for ( i = 0 ; i < dynamicTRObjs.length; i++ ){
parentObj.removeChild ( dynamicTRObjs[i] );
extraTr++;
}
}
IMO it's obvious that the benefits are huge.

Categories

Resources