Select2 each2 method - how does it work?
Asked Answered
T

3

6

I was looking through Select2 (source code) and found each2 method prototype:

$.extend($.fn, {
  each2 : function (c) {
    var j = $([0]), i = -1, l = this.length;
    while (
      ++i < l
       && (j.context = j[0] = this[i])
       && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
     );
     return this;
   }
});

My question is - how this method works? I mean - why there is while loop only with condition, without statement part? I'd really love to understand this method's flow.

Thermy answered 22/10, 2014 at 17:40 Comment(1)
Did you had a chance to review the answers? Drop a comment as this is not a one line solution and it requires clarification in case if it is not clear.Sam
G
5

When you put an expression inside a condition (for instance: if (i), if (i == null), if (++i), if (i < 2))) the expression get evaluated before its 'checked' is it true or false.

Live example:

we have var i = 0; now you call if (++i) console.log(i). The expression ++i return 1 (it is understood as truth in javascript [0 is not]) so console.log(i) logs 1.

Now let's say we have var i = 0 and if (++i && ++i) console.log(i). The first expressions returns 1 so the second is also called which returns 2. Both 1 and 2 are treated as truth so console.log(i) logs 2

Awesome. Let's have a look on the same example as above but before if we initialise var i = -1 . Now we call if (++i && ++i) console.log(i). The first ++i returns 0 which is falsity.

To continue you have to get how && works. Let me explain quickly: If you stack exp1 && exp2 && exp3 ... && expX then exp2 will be only executed(evaluated) when exp1 returns truth (for instance: true, 1, "some string"). exp3 will be executed only if exp2 is truthy, exp4 when exp3 is truth and so on...(until we hit expN)

Let's go back to f (++i && ++i) console.log(i) now. So the first ++i returns 0 which is falsity so second ++i isn't executed and whole condition is false so console.log(i) won't be executed (How ever incrementation was completed so i equals to 0 now).

Now when you get it I can tell you that while loop works the same in condition checking way. For instance var = -2 and while (++i) will be executed until ++i returns falsity (that is 0). while (++1) will be executed exactly 2 times.

TL;DR BELOW!!!

So how this works?

   while (
      ++i < l
       && (j.context = j[0] = this[i])
       && c.call(j[0], i, j) !== false
     );

I think the best way to explain is to rewrite it (:

   while ( ++i < l ) {
       if (!(j.context = j[0] = this[i])) break;
       if (!(c.call(j[0], i, j) !== false)) break;
   }

or ever less magical:

   var i = 0;
   while ( i < l ) {
       if (!(j.context = j[0] = this[i])) break;
       if (!(c.call(j[0], i, j) !== false)) break;
       i++;
   }
Ghibelline answered 26/10, 2014 at 11:20 Comment(9)
But what exactly will be executed in above example of while loop? There no statement, only condition. This is the part I have problem with. Let say my loop is executed 2 times - which part is executed?Thermy
there is. I believe (j.context = j[0] = this[i]) is the core of this algorithm. Also c.call(j[0], i, j) could do 'the thing' here.Ghibelline
Are u sure that you can rewrite it like this? Could you provide some kind of source for this kind of transformation?Thermy
if you assume that (j.context = j[0] = this[i]); always returns truth than yes. Otherwise: you can't. EDIT: actually you can't assume that. I changed my answerGhibelline
(quick explanation: var i = 2, x = 3. When we call: if(i = x = 0) console.log("YEAH"!) then console.log(..) isn't executed)Ghibelline
while loop ends with a semicolon, so nothing is executed in that loop but that empty statement, what is executed is in the control expression :)Francefrancene
The 'thing' is done by c.call(j[0], i, j). I believe Filip made a great answer for how the while statements condition is used as both the condition and the statement. I have also posted an answer attempting to explain what each2 actually does.Pawn
Thank you for your answer. Sorry for keep you waiting - the bounty is yours. :)Thermy
@Thermy thick up (green √) answerGhibelline
S
3

The purpose of the code is to add a new function each2 to the jQuery object that works similar to .each. The usage of this .each2 function is same as .each which executes over a jQuery object, takes a function to call back with 2 arg (index and value) and returns jQuery object.

The mysterious while loop,

while (
    ++i < l
    && (j.context = j[0] = this[i])
    && c.call(j[0], i, j) !== false //"this"=DOM, i=index, j=jQuery object
  );
  return this;
}

Observations:

  1. Continues "only if" all 3 conditions evaluates to true
  2. First Condition: ++i < l, a simple iterate evaluates to true until the value of i is less than count of selected elements. [i is initialized to -1, because of the usage of ++i in next line and i is used as index in later references]
  3. Second Condition: It is actually an initialization + null/undefined check. The condition is evaluated to true for all valid values and fails when there is an invalid context.
  4. Third condition: The purpose of this condition allows you to break out of the loop if the call back function return false. Same as .each where return false inside the function will break the iteration.
Sam answered 26/10, 2014 at 19:47 Comment(4)
You are correct except for two things. each2 works on arrays (or anything with a length that can be iterated over. And your 1st observation is wrong, it continues only if all 3 conditions are true.Pawn
@Pawn Thanks but, jQuery object has length and it can be iterated over however I wouldn't say the .each2 is meant for that. The .each2 built specifically for that plugin, hence the usage may be limited to just work as a utility function. And my 1st observation simply states that the while continues unless if any of the condition evaluates to false, not sure how "until" and "only if" is different in that context.Sam
I meant that each2 was not restricted to jQuery objects, but yea, I guess because each2 extends $ (the jQuery object) you are correct, it is specific the jQuery object. I think you are thinking about your first observation correctly too, but your grammar is wrong. "until" and "only if" are opposite. When you say "continues until all 3 conditions evaluates to true" you are saying that if all 3 conditions evaluate to true it will immediately stop. But the opposite is true, it will only keep going as long as all 3 are true. So just replace "until" with "if", then your answer is correct.Pawn
@Pawn I see, Thanks. Changed to "only if".Sam
P
1

As noted by OP, there is no statement after the while condition. Note the semicolon after the while loop, return this is only run after the while loop has been completed, and that is when any one of the three statements evaluate to false. If all you want to know is how the while loop is working, Filip Bartuzi's answer is probably the most complete. I will attempt to give a broader answer as to what the each2 method does.

As noted in the docs, it is just a more efficient version of jQuery's #each, or Array.prototype.forEach or Underscore's #each or lodash's #forEach that is designed specifically for select2's uses. It is used to iterate over any array or other iterable, wrapped in a jQuery object. So it is used for arrays of strings as well as jQuery collections.

The way it works is the scope (this) is the array that it was called on. It is given a function as an argument. This is exactly how the other each methods I previously mentioned are called. The function provided to each2 is called once for each item in the array. The while loop is sort of a hacky way to iterate over each item in the array, and call the function, setting the item as the context and passing the index in the array and the item as a jQuery object as the first and second arguments. Each statement in the while loop condition must be evaluated to determine if it is true, and that process is being used to actually assign values to variables, and increment i. The c.call(j[0], i, j) !== false part allows the function to terminate the loop early by returning false. If the function returns false, the while loop will stop, otherwise it will continue until i is greater than l, meaning that every item in the array has been used. Returning this afterwards just enables another method to be chained to the array after .each2.

Basically the while loop could be rewritten to:

var j = $([0]), i = 0, l = this.length, continue = true;
while (i < l) {
  i++;
  j.context = j[0] = this[i];
  continue = c.call(j[0], i, j);
  if (!continue) {
    break;
  }
}

but there are probably some performance reasons why that can't be optimized as well by the browser.

It is more fragile than the normal each methods. For example if an element in the array has a falsey value such as 0, (j.context = j[0] = this[i]) will be falsey, causing the loop to terminate. But it is only used in the specialized cases of the select2 code, where that shouldn't happen.

Let's walk through a couple of examples.

function syncCssClasses(dest, src, adapter) {
    var classes, replacements = [], adapted;

    classes = $.trim(dest.attr("class"));

    if (classes) {
        classes = '' + classes; // for IE which returns object

        $(classes.split(/\s+/)).each2(function() {
            if (this.indexOf("select2-") === 0) {
                replacements.push(this);
            }
        });
    }
    ...

^ This code is getting the classes from a dest DOM element. The classes are split into an array of strings (the string is being split on whitespace characters, that's the \s+ regular expression), each class name is an item in the array. No special jQuery work is needed here, so the 2 arguments that the function called by each2 is provided are not used. If the class starts with 'select2-' the class is added into a array called replacements. The 'select2-' classes are being copied, pretty simple.

group.children=[];
$(datum.children).each2(function(i, childDatum) { process(childDatum, group.children); });

^ For brevity I have not included the rest of this method, but it is part of the process method. In this case, each2 is being used to recursively process a node and all it's children. $(datum.children) is a jQuery selection, each 'child' (named childDatum) will be processed in turn, and it's children will go through the same process. The group.children array will be used as a collection, whatever is added to that array for each childDatum will be available, so after the each2 is run it will hold everything that has been added during the processing of all the children, grandchildren, etc.

Pawn answered 1/11, 2014 at 19:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.