I am currently upating my organisations custom JS library and one thing I am looking to introduce is querySelectorAll.
Looking at compatibility it will run in modern browser and for older browsers I will use feature detection:
if (document.querySelectorAll) {
var nodes = context.querySelectorAll(queryValue);
} else {
var nodes = context.getElementsByTagName(queryValue);
}
Are there any considerations that I should be aware of when using this method or is it good for production?
All opinions valued
Main difference between the two is that:
getElementsByTagName
...will return a "live list", and
querySelectorAll
...will not.
Since this seems to be only for selection by tag, I'd probably ditch the qsa, so you can have the live list if needed. I've got a feeling that qsa may be slower in some browsers as well, but haven't tested it.
EDIT:
This test shows a big performance difference between the two in Chrome 13.
Related
I am curious as which selector would be quicker for the browser to look up. I am interested in general rather than specific browsers.
$('accountDetailsTable').getElements('.toggler')
This will look for accountDetailsTable as if you were using document.getElementById('accountDetailsTable'); and then look for .toggler inside of the element.
$$('.toggler')
Whereas this one will return all the selectors directly. But ultimately they will both give me the same result.
So which one would be quicker? How could I test this?
performance of selectors will differ between browsers significantly. eg. with querySelector and querySelectorAll (QSA) vs without. When without QSA, with getElementsByClassName or without.
the amount of work that the selector engine needs to do will vary.
you can write this differently. 3 ways to do so, not just 2:
1. $('accountDetailsTable').getElements('.toggler')
anatomy of the above is:
get element (1 call).
call method getElements from element prototype (or element directly in old IE)
look for all childNodes that match the class selector
this will be consistent in browsers in that it gets a root node and calls methods off it. if no QSA, it will go to getElementsByClassName or walk all childNodes and filter the className property until it has a list of matches. Performance wise, this will be worse in modern browsers because it needs to chain whereas method 3 will be a direct result.
because of how selectors work in JS, unlike in CSS - it's from left to right and more qualified selectors improve performance, having something like .toggler means when there is an older browser, it needlessly needs to consider every single DOMnode in the tree (except for text nodes). always qualify the selectors when you can, i.e. div.toggler or a.toggler.
2. $$('.toggler')
if QSA is available, rely on the browser to return stuff (no extra hacks here like :not, or :contains or :has or ! (reverse combinators).
if NO QSA, it will parse the expression via the Slick parser and basically internally degrade to something like in case 1 above. The biggest difference here is the lack of context - it will run against the whole document as $$ internally does something like document.getElements('.toggler') so there are a lot more nodes to consider. Always anchor your queries to a stable upper-most common node. or pass it into the query string for qualification like in case 3
once again, this will be improved for performance by making it more qualified eg:
$$('a.toggler')
3. $$('#accountDetailsTable .toggler')
Similar to case 2 but when QSA is available, it will be faster.
When QSA it is not there, it will run against the context of the #accountDetailsTable node so it will be better than case 2.
Making this more qualified will make a difference:
$$('#accountDetailsTable td.control > a.toggler')
big discount: it will depend on how big your DOM is, how many matches are found and returned. on a simple DOM, the expected performance order may differ.
Performance optimisations of selectors are increasingly irrelevant these days.
The days of SlickText comparisons of frameworks are over and application performance has have little to do with selector speed, or you are doing something wrong.
If you do your job right, you won't need to be constantly selecting elements. You can cache things, reuse, be smart and reduce DOM lookups to a minimum.
Events etc can be attached through smart event delegation, where appropriate, completely negating the need to select and add events to multiple nodes etc - use common sense and don't get hung up on theoretical performance benchmarks. Instead, use a profiler and test your actual app and see where you lose time / CPU cycles.
You can run a simple selector like that over 50000 times in the space of a second. That's micro-benchmarking and does not measure actual real world performance inside your APP, DOM, browser etc.
More thoughts on benchmarking performance and premature optimisations: http://www.youtube.com/watch?v=65-RbBwZQdU
Careful
Write for correctness and readability first. Even if you can get extra performance by doing something, don't do it at the expense of readability if it's not critical / reused all the time. Selectors like .toggler, .new, .button etc tend to be too generic and can be re-used across the app in different parts of the DOM. You need to qualify selectors to also ensure your intended functionality will continue working when it gets moved into a different page / widget / DOM and your project gets an influx of new developers.
just my two cents, I know you already accepted the answer.
My internal MooTools knowledge got kind of rusty over the last two years, but generally speaking I expect the $$-selector to be much faster, since it has to run through the elements only once.
Why not give it a try? See this JSfiddle:
Rough HTML:
<div id="accountDetailsTable">
<div id=".toggler">1</div>
<div id=".toggler">2</div>
<div id=".toggler">3</div>
</div>
JScript (please don't comment on the bad code, its just to demonstrate the functionality):
window.addEvent('domready', function() {
var iter = 50000;
var tstart = new Date().getTime();
for (var i=0;i<iter;i++) {
var x = $('accountDetailsTable').getElements('.toggler');
}
var tend = new Date().getTime();
var tdur = tend - tstart;
console.log('$: ' + tdur + ' ms');
var tstart = new Date().getTime();
for (var i=0;i<iter;i++) {
var x = $$('.toggler');
}
var tend = new Date().getTime();
var tdur = tend - tstart;
console.log('$$: ' + tdur + ' ms');
});
That said, my tests with about 50000 iterations lead to roughly this results:
$('accountDetailsTable').getElements('.toggler') : ~ 4secs
$$('.toggler') : ~ 2secs
Results will vary across browsers, elements and much more, so this is only a rough approximation.
This said, you'll probably feel only a difference if you have that many selectors in such a short period. Yes, performance should be thought about, if you only have a few selectors in your application, it shouldn't really matter.
I prefer the $$(), not because of the better performance, but because of better readability.
element.classList is of DOMTokenList type.
Is there a method to clear this list?
I'm not aware of a "method" in the sense of a "function" associated with classList. It has .add() and .remove() methods to add or remove individual classes, but you can clear the list in the sense of removing all classes from the element like this:
element.className = "";
With ES6 and the spread operator, this is a breeze.
element.classList.remove(...element.classList);
This will spread the list items as arguments to the remove method.
Since the classList.remove method can take many arguments, they all are removed and the classList is cleared.
Even though it is readable it is not very efficient. #Fredrik Macrobond's answer is faster.
View different solutions and their test results at jsperf.
var classList = element.classList;
while (classList.length > 0) {
classList.remove(classList.item(0));
}
Here's another way to do it:
element.setAttribute("class", "")
Nowadays, classList is preferred to (remove|set)Attribute or className.
Pekaaw's answer above is good, 1 similar alternative is to set the DomTokenList.value
elem.classList.value = ''
Another option is to simply remove the class attribute:
elem.removeAttribute('class')
I recommend not using className as classList could result in faster DOM updates.
The remove() method of DOMTokenList (which is what classList is) can take multiple arguments - each a string of a classname to remove (reference). First you need to convert the classList to a plan array of classnames. Some people use Array.prototype.slice() to do that, but I'm not a fan and I think a for loop is faster in most browsers - but either way I have nothing against for loops and the slice often feels like a hack to me. Once you have the array you simply use apply() to pass that array as a list of arguments.
I use a utility class I wrote to accomplish this. The first method is the one you are looking for, but I'm including slightly more for your reference.
ElementTools.clearClassList = function(elem) {
var classList = elem.classList;
var classListAsArray = new Array(classList.length);
for (var i = 0, len = classList.length; i < len; i++) {
classListAsArray[i] = classList[i];
}
ElementTools.removeClassList(elem, classListAsArray);
}
ElementTools.removeClassList = function(elem, classArray) {
var classList = elem.classList;
classList.remove.apply(classList, classArray);
};
ElementTools.addClassList = function(elem, newClassArray) {
var classList = elem.classList;
classList.add.apply(classList, newClassArray);
};
ElementTools.setClassList = function(elem, newClassArray) {
ElementTools.clearClassList(elem);
ElementTools.addClassList(elem, newClassArray);
};
Please note that I have not thoroughly tested this in all browsers as the project I am working on only needs to work in a very limited set of modern browsers. But it should work back to IE10, and if you include a shim (https://github.com/eligrey/classList.js) for classList, it should work back to IE7 (and also with SVGs since Eli Grey's shim adds support for SVG in unsupported browsers too).
An alternative approach I considered was to loop backwards through the classList and call remove() on classList for each entry. (Backwards because the length changes as you remove each.) While this should also work, I figured using the multiple arguments on remove() could be faster since modern browsers may optimize for it and avoid multiple updates to the DOM each time I call remove() in a for loop. Additionally both approaches require a for loop (either to build a list or to remove each) so I saw no benefits to this alternative approach. But again, I did not do any speed tests.
If somebody tests speeds of the various approaches or has a better solution, please post.
EDIT: I found a bug in the shim which stops it from correctly adding support to IE11/10 for multiple arguments to add() and remove(). I have filed a report on github.
The author of this page: http://24ways.org/2011/your-jquery-now-with-less-suck asserts that the jQuery selector
$('#id').find('p') is faster than $('#id p'), although that presumably produce the same results if I understand correctly. What is the reason for this difference?
Because $('#id').find('p') is optimized to do...
document.getElementById('id').getElementsByTagName('p');
...whereas I'm guessing $('#id p') will either use querySelectorAll if available, or the JavaScript based selector engine if not.
You should note that performance always has variations between browsers. Opera is known to have an extremely fast querySelectorAll.
Also, different versions of jQuery may come up with different optimizations.
It could be that $('#id p') will be (or currently is) given the same optimization as the first version.
It’s browser specific since jQuery uses querySelectorAll when it’s available. When I tested in WebKit it was indeed faster. As it turns out querySelectorAll is optimized for this case.
Inside WebKit, if the whole selector is #<id> and there is only one element in the document with that id, it’s optimized to getElementById. But, if the selector is anything else, querySelectorAll traverses the document looking for elements which match.
Yep, it should be possible to optimize this case so that they perform the same — but right now, no one has. You can find it here in the WebKit source, SelectorDataList::execute uses SelectorDataList::canUseIdLookup to decide whether to use getElementById. It looks like this:
if (m_selectors.size() != 1)
return false;
if (m_selectors[0].selector->m_match != CSSSelector::Id)
return false;
if (!rootNode->inDocument())
return false;
if (rootNode->document()->inQuirksMode())
return false;
if (rootNode->document()->containsMultipleElementsWithId(m_selectors[0].selector->value()))
return false;
return true;
If you were testing in a non-WebKit browser, it’s possible that it is missing similar optimizations.
In a piece of example code I wrote
var as = toArray(document.getElementsByClassName("false")).filter(function (el) {
return el.tagName === "A";
});
And I was thinking I could replace that with
var as = document.querySelectorAll("a.false");
Now after reading the following facts
Pretend browser support isn't an issue (we have shims and polyfills).
Pretend your not in your generic jQuery mindset of you shall use QSA for getting every element.
I'm going to write qsa instead of document.querySelectorAll because I'm lazy.
Question: When should I favour QSA over the normal methods?
It's clear that if your doing qsa("a") or qsa(".class") or qsa("#id") your doing it wrong because there are methods (byTagName, byClassName, byId) that are better.
It's also clear that qsa("div > p.magic") is a sensible use-case.
Question: But is qsa("tagName.class") a good use-case of QSA?
As a futher aside there are also these things called NodeIterator
I've asked a question about QSA vs NodeIterator
You should use QSA when the gEBI, gEBN, gEBCN do not work because your selector is complex.
QSA vs DOM parsing is a matter of preference and what your going to be doing with the returned data set.
If browser support was not an issue I would just use it everywhere. Why use 4 different methods (...byId, ...byTagName, ...byClassName) if you could just use one.
QSA seems to be slower (...byId), but still only takes a few miliseconds or less. Most of the times you only call it a few times, so not a problem. When you hit a speed bottleneck you could always replace QSA with the appropriate other one.
I've set up some tests for you to mess around with. It appears that QSA is a lot slower. But if you are not calling it that much, it shouldn't be a problem.
http://jsfiddle.net/mxZq3/
EDIT - jsperf version
http://jsperf.com/qsa-vs-regular-js
I'm doing a jQuery multiple selector find:
element.find("fieldset, input[type=hidden], input[type=text], :radio")
and in Chrome version 1 it gives this error "INVALID_NODE_TYPE_ERR: DOM Range Exception 2" on line 722 of jquery's selector.js
aRange.selectNode(a);
in context:
function(a, b) {
var aRange = a.ownerDocument.createRange(), bRange = b.ownerDocument.createRange();
aRange.selectNode(a);
aRange.collapse(true);
bRange.selectNode(b);
bRange.collapse(true);
var ret = aRange.compareBoundaryPoints(Range.START_TO_END, bRange);
if (ret === 0) {
hasDuplicate = true;
}
return ret;
}
in this case, a is a HTML hidden input field. From what I can find, it seems to be an issue with the older webkit version, as this error doesn't occur in the new beta of Chrome (probably because it never hits this code because it implements document.documentElement.compareDocumentPosition see selector.js#703).
To step around this problem, I've replaced the multi-selector with four single selects which I merge together which works fine, but it's really ugly:
elements = element.find('fieldset')
.add(element.find('input[type=hidden]'));
.add(element.find('input[type=text]'));
.add(element.find(':radio'));
Is this really the only way around this, or is there something else I can do?
UPDATE There is a thread about this on the Sizzle discussion forum, a possible patch to the Sizzle (jQuery selector) code has been posted, this may find its way into jquery core. It seems to only be an issue when doing a multiple selector on dynamic code
if the problem is the web browser, then sadly there is nothing you can do but wait for an update, or use the multiple selectors and merge the result sets. From what it looks like, this wouldn't be a big performance hit at all, and thus I wouldn't worry about it.
Have you tried...
element.find(['fieldset', 'input[type=hidden]', 'input[type=text]', ':radio'])
?
For reference the entirety of the DOM and rendering is just Apple's WebKit so any bugs you see should be reported to http://bugs.webkit.org -- Chrome doesn't have its own unique engine.