Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Behaviour of children() and find() when using :first

Tags:

jquery

I'm a little bit confused by the behaviour when using children() and find() with :first.

Consider the following markup:

<div class="parent">
    <div>1</div>
    <div class="child">2</div>
    <div class="child">3</div>
</div>

<div class="parent">
    <div>1</div>
    <div class="child">2</div>
    <div class="child">3</div>
</div>

From what I understand, the following should return the same collection of elements, as there are no grandchildren (which is the only difference stated between find() and children() in the documentation).

However, if I add a class to the first .child, the results are different:

$('.parent').find('.child:first').addClass('active');

Results in the following markup:

<div class="parent">
    <div>1</div>
    <div class="child active">2</div>
    <div class="child">3</div>
</div>

<div class="parent">
    <div>1</div>
    <div class="child active">2</div>
    <div class="child">3</div>
</div>

When doing the same thing using the children() method, I get:

<div class="parent">
    <div>1</div>
    <div class="child active">2</div>
    <div class="child">3</div>
</div>

<div class="parent">
    <div>1</div>
    <div class="child">2</div>
    <div class="child">3</div>
</div>

Why?

Here's a fiddle

like image 305
billyonecan Avatar asked Aug 14 '14 11:08

billyonecan


1 Answers

Nice question!

TL;DR

The difference is that find searches individually starting from each wrapped element and then aggregates the results, while children gets a single aggregate pool of candidate results which are then filtered based on the specified selector. This results in selectors like :first giving a different result if the original jQuery object wraps more than one element.

The reason for this is understandable (children knows from the get-go that all of its possible matches share a very important property in the DOM, so it makes sense to narrow down the candidate list up front for performance reasons), but IMO you can't call the result anything else than a bug in the current implementation.

$.fn.find

find produces two matches like that because it runs a search on each of the wrapped elements of the current jQuery object and then aggregates the results. So for each .parent we match the first descendant in document order that is a .child.

Here's the source:

function (selector) {
    var i, ret = [],
        self = this,
        len = self.length;

    if (typeof selector !== "string") {
        return this.pushStack(jQuery(selector).filter(function () {
            for (i = 0; i < len; i++) {
                if (jQuery.contains(self[i], this)) {
                    return true;
                }
            }
        }));
    }

    for (i = 0; i < len; i++) {
        jQuery.find(selector, self[i], ret);   // ** IMPORTANT **
    }

    // Needed because $( selector, context ) becomes $( context ).find(selector)
    ret = this.pushStack(len > 1 ? jQuery.unique(ret) : ret);
    ret.selector = this.selector ? this.selector + " " + selector : selector;
    return ret;
}

Everything happens in the line marked "IMPORTANT": jQuery.find is an alias to Sizzle, which appends its results to ret each time. Obviously if you do .find(":first") on a jQuery object that wraps N elements with each element having at least one descendant, you will get back exactly N results.

$.fn.children

children takes another route: for each wrapped element it traverses the DOM to get hold of its children, and then filters the results as a whole based on the selector. Obviously in this case this would leave at most one element as the final result.

Here's how it happens:

function (until, selector) {
    var ret = jQuery.map(this, fn, until); // ** IMPORTANT 1 **

    if (name.slice(-5) !== "Until") {
        selector = until;
    }

    if (selector && typeof selector === "string") {
        ret = jQuery.filter(selector, ret); // ** IMPORTANT 2 **
    }

    if (this.length > 1) {
        // Remove duplicates
        if (!guaranteedUnique[name]) {
            ret = jQuery.unique(ret);
        }

        // Reverse order for parents* and prev-derivatives
        if (rparentsprev.test(name)) {
            ret = ret.reverse();
        }
    }

    return this.pushStack(ret);
}

This is not as self-explanatory because the code is shared with a bunch of methods that do structural traversal of the DOM (parent, next, prev, siblings, etc), but again the relevant part of the code is very simple: The line "IMPORTANT 1" collects the results of the structural traversal ("get the children") inside ret, and those results as a whole are filtered based on the selector (".child:first"). This will finally leave at most one result.

like image 101
Jon Avatar answered Sep 28 '22 08:09

Jon