-1

Rather than use $('.list').find('li:not(.done):last'), which returns all LI that is not .done and is the last of every .list, how can I use functions instead?

For instance, $('.list').find('li').not('.done').last() would return just one item, because it searches all .list for LI that aren't .done and then just returns the last one.

There are a few reasons I want to do this. Primarily, typically using functions are faster than CSS selectors (in some cases, especially when you split them up into multiple functions as jQuery doesn't have to manually split up the selectors, and oftentimes jQuery functions are just mapped directly to existing CSS selectors anyway). But anyway, curiosity is my main reason at the moment, and performance is secondary.

Sample code (not entirely representative of the actual code, but the structure is similar):

<ul class="list">
  <li>One</li>
  <li class="done">Two</li>
</ul>

<ul class="list">
  <li class="done">Three</li>
  <li>Four</li>
</ul>

I want to return the nodes for:

One and Four.

Paul Sweatte
  • 24,148
  • 7
  • 127
  • 265
Gary
  • 3,891
  • 8
  • 38
  • 60
  • Perhaps it depends on the selector. I thought there were some cases where using the jQuery functions, or at least splitting up selectors, were much faster. Also, if I just use `.find('li')` then the DOM stops changing (according to Firefox Inspect). If I re-add the `:not()` portion, then every `.list` flashes whenever the function is run. I assume this indicates that the DOM is changing? – Gary Oct 04 '14 at 17:31
  • 1
    a pure css selector will be faster than using methods... also using an selector should not change the dom – Arun P Johny Oct 04 '14 at 17:32
  • 1
    In any case, is there a way to get the last `LI` of every `.list` that also meets my filter criteria? – Gary Oct 04 '14 at 17:34
  • Quite right, the above is a bit of a side discussion. – T.J. Crowder Oct 04 '14 at 17:34
  • so if the last `li` is `.done` you don't want to return that... or do you want to return any previous `li` which is not `.done` – Arun P Johny Oct 04 '14 at 17:37
  • I want to return the last `li` which is not `.done`. Therefore, I want to return at least something, for every `.list`. – Gary Oct 04 '14 at 17:38
  • for that requirement... your selector seems pretty fine... – Arun P Johny Oct 04 '14 at 17:44
  • Too many false assumptions in this question. Clean up your question to a simple and real question and you will probably get more takers. Try making your question into something like: "what is a fast way to accomplish X"? – jfriend00 Oct 04 '14 at 17:48
  • Don't do that! If you use a valid CSS selector, jQuery can take advantage of the performance boost provided by the native DOM `querySelectorAll()`, so it will be faster. However, note than if you use jQuery extensions like `:last`, jQuery can't use `querySelectorAll()`. Then, you should use `$('.list li:not(.done)').last()` – Oriol Oct 04 '14 at 17:49
  • @Oriol: That's the point. `$('.list li:not(.done)').last()` doesn't do what the OP wants (the last in *each* list), and he's asking how to do it. – T.J. Crowder Oct 04 '14 at 17:50
  • @Gary: Re "Perhaps it depends on the selector," yes indeed: `:last` is a jQuery thing, not a CSS thing, and so the entire selector has to be processed in JavaScript rather than by the browser's built-in selector engine (which is *dramatically* faster). Sticking to standard CSS selectors lets jQuery leverage the browser's engine for speed. – T.J. Crowder Oct 04 '14 at 17:51
  • Maybe the problem I'm experiencing is a new Firefox thing. Basically, I have `setInterval` for this function that checks for any new items with this selector. I'm using it in a Greasemonkey script, meaning on sites that are not mine. Every time this function runs, as long as it has `:first`, `:last`, `:not`, etc., the corresponding selector in the Firefox Inspect panel will flash. And my CPU usage in Firefox spikes to 100% every time the function runs. – Gary Oct 04 '14 at 18:03
  • Actually scratch the CPU spike, that's just because I had the Inspect panel open. But my point still stands in that it seems like even simply using jQuery in Greasemonkey to find elements, the element will flash in the Inspect panel. I'm not sure if that's new, but I think it is. – Gary Oct 04 '14 at 18:15

3 Answers3

1

I'm not coming up with a way to do it that doesn't require looping, e.g.:

$(".list").map(function() {
  var items = $(this).find("li").not(".done");
  return items[items.length - 1];
});

// Old way
$(".list").find("li:not(.done):last").css("color", "red");

// Just to show it's wrong (as the question says)
$(".list").find("li").not(".done").last().css("background-color", "yellow");

// Alternative
$(".list").map(function() {
  var items = $(this).find("li").not(".done");
  return items[items.length - 1];
}).css("border", "1px solid black");
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<ul class="list">
  <li>One</li>
  <li>Two</li>
  <li class="done">Three (done)</li>
</ul>
<ul class="list">
  <li>One</li>
  <li>Two</li>
  <li class="done">Three (done)</li>
</ul>

The reason you might think using selectors is slower is that selectors like :last are jQuery extensions, not true CSS selectors, meaning that Sizzle (jQuery's selector engine) has to process them in JavaScript, rather than offloading them to the (much faster) selector engine built into the browser. If you restrict yourself to CSS selectors the browser implements, typically the selector will win (modulo other factors, and YMMV). I can't account for your seeming to see the DOM being changed by :not(.done) (which is a valid CSS selector), unless Sizzle does something It Really Shouldn't to process that (since Sizzle supports complex selectors within :not whereas CSS doesn't).

T.J. Crowder
  • 1,031,962
  • 187
  • 1,923
  • 1,875
  • I'm perhaps missing something, but wouldn't `$('.done').prev('li:not(.done)').css('box-shadow', 'inset 0 0 5px #f90');` suffice as a jQuery selection method? – David Thomas Oct 04 '14 at 18:02
  • 1
    Nice, I like the `.map` method. That is closest to what I am looking for. – Gary Oct 04 '14 at 18:04
  • @DavidThomas: `prev` only considers the immediately-previous sibling, and I assume there can be more than one not-done and done item in a list, so probably not. That's not to say something *like* that wouldn't be an option, but I'm pretty sure you'd still end up looping. – T.J. Crowder Oct 04 '14 at 18:09
  • @Gary: I've made the above *slightly* more efficient, but you may find that Sizzle's `:last` implementation is just as efficient or more so. (Or, of course, not. :-) ) – T.J. Crowder Oct 04 '14 at 18:10
0

You don't actually need a function. Any of the following selectors would return the nodes for One and Four:

  • $('.list li:not(.done)')
  • $('.list li').not('.done')
  • $('li:not(.done)')

Using $('.list li:last-child').not('.done') would return those items that are both the last child and don't have a done class (Four).

That said, if you really want to use a function, you could go with jQuery's .contents() and .filter() methods.

$('.list').contents().filter( function(i,v){
  if( v.nodeType === 1 && !$(v).hasClass('done') ){
    return v; // Returns 'One' and 'Four'
  }
});

Here's a fiddle of all of the above.

webinista
  • 3,750
  • 1
  • 20
  • 21
0

There are a few reasons I want to do this. Primarily, typically using functions are faster than CSS selectors There are LIs that I don't want, and I want the last of every list that doesn't match a class

The fastest method would be XPath, since it is native:

//*[@class='list']/li[@class!='done']/*[last()]

But anyway, curiosity is my main reason at the moment, and performance is secondary.

jQuery 1.0 supported XPath, then switched to the selector engine which eventually became Sizzle:

Lately, I’ve been doing a lot of work building a parser for both XPath and CSS 3 – and I was amazed at just how similar they are, in some respects – but wholly different in others. For example, CSS is completely tuned to be used with HTML, with the use of #id (to get something by ID) and .class (to get something by its class). On the other hand, XPath has to ability to traverse back up the DOM tree with .. and test for existence with foo[bar] (foo has a bar element child). The biggest thing to realize is that CSS Selectors are, typically, very short – but woefully underpowered, when compared to XPath.

which were removed in 1.2:

Since XPath selectors were removed form jQuery in 1.2, a new XPath Selector Plugin has been introduced. You can use this plugin to give yourself the CSS/XPath hybrid selectors that have existed in jQuery since its creation.

DOM traversal techniques include:

Tree Walker

An implementation using the DOM Level 2 Tree Walker methods. Builds a generic filter function and traverses through all elements.

XPath

Building a single expression and letting the XPath engine traverse through the document, finding all the relevant elements.

Hybrid

Mixes an XPath and DOM implementation; using XPath wherever possible.

References

Paul Sweatte
  • 24,148
  • 7
  • 127
  • 265