ded/qwery — pull request #53 by rvagg (2011-11-02 14:54:11+00:00)
Title: Improved support for older browsers, relationship-first element-rooted queries, ++

**So** sorry about the inevitable gitshock you're about to receive when you look at this, I've been sitting on it for a bit too long so the diff looks quite dramatic. I imagine you'll want to take some time with this.

My original objectives were (1) to fix up a bunch of broken cases for the non-CSS3 path and (2) introduce support for putting a relationship selector at the beginning of queries where you pass an element as the context, e.g. `Q('> .foo', Q('#bar'))` which is supported by other selector engines (and is quite handy!).

I ended up with both of those plus some more stuff. I even found an extra chunk of speed in the process. Unfortunately file size is up a tad as you can see. But, as a bonus I've included a much improved benchmark suite that is just waiting to be expanded.

Highlights:

* Removed reliance on mutable globals
* Fixed some minor regex issues including better whitespace handling in `normalizr` (totally removes all unnecessary whitespace now including leading and trailing spaces and spaces around commas, previously it left a lot of these in as blank tokens and/or " " dividers).
* Refactor out duplicate class-regex code
* Introduce `each()` to simplify some of the loops. Not *quite* as fast as `for` in older browsers so it's not used for heavily used loops
* Introduced `arrayify()` as a 2D compliment to `flatten()`, specifically to make NodeLists into arrays. I tried a couple of tactics here including using `Array.prototype.split` which is super fast on FF but slows Chrome down so I reverted to a compromise that satisfies all browsers, with no need to feature-detect for the oldies. There's a big speed bump from avoiding `flatten()` for the simple cases in new browsers so Chrome now runs faster there's a big speed bump in FF.
* `getAttr()` is now constructed based on feature checking so there's fewer runtime conditionals
* `ancestorMatch()` has been rewritten, this turned out to be the root of a lot of selector bugs I was looking for; specifically the break on 'if found' which meant that not all relationship options were considered, only the first that was found, which can cause problems for later tokens. Only a problem where there are more than a couple of tokens and only when there are relationship dividers. Some queries returned more elements than they should and some returned not enough. I've put a few of them into the test suite. The new `ancestorMatch()` is recursive so it can easily consider all relationships, which is important for the dividers that can yield multiple matches.
* There's now a single regex match performed in `qwery()` for the speed-up cases, instead of one for `idOnly`, one for `tagOnly` and one for `classOnly` (which has been moved up out of `select()`).
* The new `collectSelector()` is introduced to work with `each()` to handle the cases where the root is not doc and you have a relationship selector first. I think you'll find the most controversial change in here with the hack to work with root elements that don't have an id (the late Prototype selector engine introduced this, now Sizzle uses it too).
* Lots of scary looking refactoring inside `select()` that's really not that scary.

Then there's the benchmark.html changes. I found myself messing around in here to try and work out which were the slowest paths through the code so I decided that it'd just be easier if I expanded it.
You can now define batches of benchmarks to run for different categories of selectors. I've included the original benchmark queries in there at the top as "Mixed bag" and then come up with some other basic categories; though I haven't put a whole lot of time or thought into the best makeup of each of these, something for someone to do later.
The benchmarks use a crude calibration to work out how many iterations to run on the current browser. Results blocks are written into the DOM at runtime with a 'winner' indicator for each benchmark category. `setTimeout()` is used to make sure the browser doesn't lock up and so you can see it working. Now it's pretty easy to add and remove queries to see where it could do with some work. Kind of fun too.

Lastly, IE8 performance is still pretty terrible. While I think there's been a performance boost across the board, IE8 still lags behind other selector engines because the native `querySelectorAll` is underutilised. This is a job for the future but I could imagine perhaps a regex to check for queries that will work in IE8's qSA, at the moment we just do the `tagAndOrClass` check. Probably pretty important to address this because of the IE8 market share and the fact that it's not going anywhere for a long time to come.

Questions? Criticisms?