jQuery Custom Selector Execution Exploration

Posted January 15, 2010 at 9:06 PM by Ben Nadel

Tags: Javascript / DHTML

The other day, on my blog post about creating custom jQuery selectors, Scott Smith mentioned to me that he was having some performance issues with a custom selector. Specifically, he mentioned that in some experimentation, he noticed that his custom selector was being run against more nodes than he expected. Until then, I had never really thought deeply about how a custom jQuery selector was actually executed, so I figured I'd do some experimentation of my own.

In the following code, I have a Span element wrapped in a series of nested Div elements. Two of the outer-most Div elements have the class, "parent":

  • <div class="parent">
  • <div class="parent">
  • <div>
  • <span>Here is some inner text.</span>
  • </div>
  • </div>
  • </div>

When I run the code (below), I'm going to locate the Span element; then, I'm going to call the parents() method on it, using the following selector:

div.foo:testSelctor.bar

Notice that we are running 4 tests on each node returned by parents():

  • Test for the Div tag.
  • Test for the class, ".foo".
  • Test for the custom jQuery selector, ":testSelector".
  • Test for the class, ".bar".

The custom jQuery selector, ":testSelector," simply logs its invocation arguments to the console such that we can see where in the process it is being called.

Now that you have an idea of what I'm planning to do, let's take a look at the actual code:

  • <!DOCTYPE HTML>
  • <html>
  • <head>
  • <title>jQuery Custom Selector Execution Exploration</title>
  • <style type="text/css">
  •  
  • div {
  • border: 1px solid #CCCCCC ;
  • padding: 10px 10px 10px 10px ;
  • }
  •  
  • div.parent {
  • border-color: gold ;
  • }
  •  
  • </style>
  • <script type="text/javascript" src="jquery-1.4.js"></script>
  • <script type="text/javascript">
  •  
  • // This simply outputs the arguments passed to the
  • // selector for debugging purposes.
  • jQuery.expr[ ":" ].testSelector = function(
  • node,
  • index,
  • properties,
  • collection
  • ){
  • // Output the custom selector arguments.
  • console.log( node, index, collection );
  • return( true );
  • };
  •  
  •  
  • // When the DOM is ready, initialize.
  • jQuery(function( $ ){
  •  
  • // Get all of the spans.
  • var spans = $( "span" );
  •  
  • // Get all the parent divs with the class "parent".
  • //
  • // NOTE: This selector is using a custom selector
  • // sandwiched bewteen two CLASS selectors.
  • var parents = spans.parents(
  • "div" +
  • ".foo" +
  • ":testSelector" +
  • ".bar"
  • );
  •  
  • // Add a red, thick border.
  • parents.css( "border", "2px solid red" );
  •  
  • // Output the parents.
  • console.log( ".... parents ...." );
  • console.log( parents );
  • });
  •  
  • </script>
  • </head>
  • <body>
  •  
  • <h1>
  • jQuery Custom Selector Execution Exploration
  • </h1>
  •  
  • <div class="parent">
  • <div class="parent">
  • <div>
  • <span>Here is some inner text.</span>
  • </div>
  • </div>
  • </div>
  •  
  • </body>
  • </html>

As you can see, the ":testSelector" custom jQuery selector simply outputs the node being examined, its index, and the collection from which it came. When we run this code, here is what FireBug is telling me:

 
 
 
 
 
 
jQuery Custom Selector Execution Exploration - Some Cases Do Not Use Progressing Trimming Of The Node Collection. 
 
 
 

This is very interesting! Look at the nodes in the collection being examined by the custom selector:

  • div
  • div.parent
  • div.parent
  • body
  • html

This completely changes the way I thought selectors were executed. I understand that Sizzle runs tree traversal in reverse; however, as my tests are all on the same node, I figured that would not matter. Of course, this has nothing to do with forward vs. reverse selecting - this has to do with progressive collection trimming. It appears that each selector execution does not perform any progressive collection trimming; meaning, neither the "div" selector, nor the ".foo" selector, nor the ".bar" selector had any pre-trimming affect on the node collection before it was passed into the testSelector custom jQuery selector method.

This is fascinating! As far as ".foo", ".bar", and ":testSelector" go, I figured I might not be able to count on order; but, I was almost certain that the "Div" tag selector would have done at least some pre-trimming of the collection. After all, I thought Sizzle used the getElementsByTagName() method.

Of course, it's not quite that straightforward! This exploration used the parents() method; if I were to replace the above use of parents() with any of the following direct jQuery selections:

  • $( "div.foo:testSelector.bar" );
  • $( "div.foo:testSelector" );
  • $( ".foo:testSelector" );

... then, the ":testSelector" method would never get executed. As it turns out, when you do direct selections, jQuery does perform progressive trimming of the target node collection. I assume it does this using variations of getElementsByTagName(), getElementsByClassName(), and whatever new query methods browsers now support, before the node collection is passed off to (or rather, not off to) ":testSelector."

Ultimately, it doesn't really matter how the node collections get trimmed, since undesired nodes will be removed one way or another based on your selector; but, it's something that I have never thought deeply about before, and it's interesting to see how selector behavior changes (behind the scenes) based on the context of the selection.




Reader Comments

Jan 19, 2010 at 1:08 PM // reply »
14 Comments

Ben:

We do a weekly showcase of any new, interesting stuff to show our group. This week I will be doing a modified presentation of the information in this post (as well as turning some folks on to your site :) This is great stuff and really important when you are going for performant code. Thanks for delving into this topic (:


Jan 19, 2010 at 2:00 PM // reply »
11,314 Comments

@Kristopher,

My pleasure. jQuery is really fascinating stuff and I'm constantly discovering more ways to use it. I hope your presentation goes well.


Jan 21, 2010 at 3:35 AM // reply »
2 Comments

Hi Ben,
How to get the jQuerify button in FireBug?


Jan 22, 2010 at 9:44 PM // reply »
11,314 Comments

@Ngoc,

I am not sure what you mean? I don't know what "jQuerify button" is.


Jan 28, 2010 at 10:39 AM // reply »
2 Comments

@Ben,
In the above screenshot (http://www.bennadel.com/resources/uploads/jquery_custom_selector_execution.gif), in FireBug, there's a button "jQuerify" beside button Clear and Profile, how to get it?
Thank you.


Jan 28, 2010 at 10:41 AM // reply »
11,314 Comments

@Ngoc,

Hmm, funky. I never noticed that before. I am not even sure what it does.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
Jun 19, 2013 at 2:01 PM
Experimenting With The Amazon Simple Storage Service (S3) API Using ColdFusion
I have coincidentally been beating my head against the S3 API for the last week or so. One big "gotcha" I had to work around was file names and paths containing spaces. Remember to URL Enco ... read »
Jun 19, 2013 at 1:27 PM
Using Slice(), Substring(), And Substr() In Javascript
very good article. By the way IE supports negative values in substr or slice in verson 10. ... read »
Jun 19, 2013 at 11:33 AM
Filter vs. ngHide With ngRepeat In AngularJS
In your assessment, is it correct to say that given a list of say 500 items its more performant to use the `ngHide` method over the `filter` method? ... read »
Jun 19, 2013 at 10:18 AM
ColdFusion Path Usage And Manipulation Overview
Anyone happen to know if the file created by getTempFile will be automatically removed at any point? Nothing mentioned in the docs, and restarting CF doesn't remove them, so it seems it needs manu ... read »
Jun 19, 2013 at 9:41 AM
Working With Inherited Collections In AngularJS
I actually just ran into this same situation with a demo I was putting together. Your implementation of multi-lvl $scope's > Mine :) ... read »
Jun 19, 2013 at 8:17 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
@Prateek, to match a word or text you should use .toContain('word') that's a jasmine reference. website is : http://pivotal.github.io/jasmine/ ... read »
Jun 19, 2013 at 8:10 AM
My Experience With AngularJS - The Super-heroic JavaScript MVW Framework
Hi Guys, Actually i am doing e2e test of angular js of my project but i am not getting one thing that is how to press enter key through the test when my form is filled as i am not using a button but ... read »
Jun 18, 2013 at 9:20 PM
Mapping AngularJS Routes Onto URL Parameters And Client-Side Events
I couldn't find examples of passing multiple arguments using the when() routing statement so figured out through trial and error that you can pass multiple arguments using the following format: .whe ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools