jquery.jsを読み解く

第7回jQueryライブラリ(1637行目~1794行目)

ここ最近、JavaScript開発者の間で「jQuery」はかなりの盛り上がりを見せていますが、ついにGoogle検索キーワードの人気でも「prototype.js」を上回ったようです。まだ使ったことがないという方も、ぜひこの機会に触れてみてはいかがでしょうか。まず試しに使ってみる→内部がどうなっているか気になる→jQueryのソースを読む、というプロセスを経ることでよりjQueryへの理解が深まるのではないかと思います。

jQueryとprototype.jsの人気(by Google Trends)
jQueryとprototype.jsの人気(by Google Trends)

さて、今回はjQuery.filterなどの内部処理用メソッドを説明していきます。

jQuery.classFilter()

1637:   classFilter: function(r,m,not){
1638:     m = " " + m + " ";
1639:     var tmp = [];
1640:     for ( var i = 0; r[i]; i++ ) {
1641:       var pass = (" " + r[i].className + " ").indexOf( m ) >= 0;
1642:       if ( !not && pass || not && !pass )
1643:         tmp.push( r[i] );
1644:     }
1645:     return tmp;
1646:   },
1647: 

jQuery.classFilterメソッドは、引数 r の要素から引数 m というクラス名が指定されている要素のみを抽出するメソッドです。1638行目および1641行目で、クラス名の前後に空白を付与してデリミタとして利用しています。また、1642行目のif文により、第3引数 not がtrueの場合は逆の動作になり、クラス m が指定されていない要素のみが返されます。

jQuery.filter()

jQuery.filterメソッドは、find(),filter()などから利用される内部処理用のメソッドです。長いのでいくつかに区切って見ていきましょう。

1648:   filter: function(t,r,not) {
1649:     var last;
1650: 
1651:     // Look for common filter expressions
1652:     while ( t && t != last ) {
1653:       last = t;
1654: 
1655:       var p = jQuery.parse, m;
1656: 
1657:       for ( var i = 0; p[i]; i++ ) {
1658:         m = p[i].exec( t );
1659: 
1660:         if ( m ) {
1661:           // Remove what we just matched
1662:           t = t.substring( m[0].length );
1663: 
1664:           m[2] = m[2].replace(/\\/g, "");
1665:           break;
1666:         }
1667:       }
1668: 
1669:       if ( !m )
1670:         break;
1671: 

1648行目から見ていきましょう。第1引数 t にセレクタ式、第2引数 r に要素配列が渡されてきます。第3引数 not はフィルタ条件を反対にするboolean値です。

1653行目でlastの値として、セレクタ式 t を設定しています。このセレクタ式内の文字列に対して繰り返し処理を行っていきます。順に処理を行いlastの値が変化していくため、lastの値が変化していなかったら終了となります。

1655行目のjQuery.parseは1421行目で定義されているセレクタ式評価用の正規表現になります。1657行目のfor文は、そのjQuery.parseの結果でマッチするものがあれば変数 m に格納して、マッチした文字列を除いたものをtに格納します。これ以降は、このマッチした文字列に対し処理を行っていきますので、マッチする文字列がなければ、1670行目にてwhileループを抜けます。

1672:       // :not() is a special case that can be optimized by
1673:       // keeping it out of the expression list
1674:       if ( m[1] == ":" && m[2] == "not" )
1675:         // optimize if only one selector found (most common case)
1676:         r = isSimple.test( m[3] ) ?
1677:           jQuery.filter(m[3], r, true).r :
1678:           jQuery( r ).not( m[3] );
1679: 
1680:       // We can get a big speed boost by filtering by class here
1681:       else if ( m[1] == "." )
1682:         r = jQuery.classFilter(r, m[2], not);
1683: 
1684:       else if ( m[1] == "[" ) {
1685:         var tmp = [], type = m[3];
1686:         
1687:         for ( var i = 0, rl = r.length; i < rl; i++ ) {
1688:           var a = r[i], z = a[ jQuery.props[m[2]] || m[2] ];
1689:           
1690:           if ( z == null || /href|src|selected/.test(m[2]) )
1691:             z = jQuery.attr(a,m[2]) || '';
1692: 
1693:           if ( (type == "" && !!z ||
1694:              type == "=" && z == m[5] ||
1695:              type == "!=" && z != m[5] ||
1696:              type == "^=" && z && !z.indexOf(m[5]) ||
1697:              type == "$=" && z.substr(z.length - m[5].length) == m[5] ||
1698:              (type == "*=" || type == "~=") && z.indexOf(m[5]) >= 0) ^ not )
1699:               tmp.push( a );
1700:         }
1701:         
1702:         r = tmp;
1703: 

1674行目は、セレクタ式に「:not(selector)」の書式が含まれていた場合の処理で、第3引数notをtrueにして、自身のjQuery.filterメソッドを呼び出し直します。ただし、1676行目の三項式によりfoo#barのように「:」⁠#」⁠[」⁠.」を間に含む場合はnot()メソッドを利用します。

1681行目は、セレクタ式が「.foo」のようなクラス指定の場合の処理で、この場合はjQuery.classFilterメソッドを利用します。

1684行目は、[attribute=value]のような属性フィルタ式の場合の処理になります。1688行目で変数aに検索対象の要素を設定し、jQuery.propsまたは属性プロパティを直接参照して結果を変数zに格納します。値が見つからないか、attributeの値が href, src, selected の場合は、次にjQuery.attrメソッドを用いて値を取得します。そして1693行目ですが、関係演算子が ⁠=」⁠!=」⁠^=」⁠$=」⁠$=」⁠*=」⁠~=」の場合の処理を1つのif文だけで処理しています。論理演算子||や&&をうまく活用して、tこの条件に合致したものを配列に格納します。

1704:       // We can get a speed boost by handling nth-child here
1705:       } else if ( m[1] == ":" && m[2] == "nth-child" ) {
1706:         var merge = {}, tmp = [],
1707:           // parse equations like 'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'
1708:           test = /(-?)(\d*)n((?:\+|-)?\d*)/.exec(
1709:             m[3] == "even" && "2n" || m[3] == "odd" && "2n+1" ||
1710:             !/\D/.test(m[3]) && "0n+" + m[3] || m[3]),
1711:           // calculate the numbers (first)n+(last) including if they are negative
1712:           first = (test[1] + (test[2] || 1)) - 0, last = test[3] - 0;
1713:  
1714:         // loop through all the elements left in the jQuery object
1715:         for ( var i = 0, rl = r.length; i < rl; i++ ) {
1716:           var node = r[i], parentNode = node.parentNode, id = jQuery.data(parentNode);
1717: 
1718:           if ( !merge[id] ) {
1719:             var c = 1;
1720: 
1721:             for ( var n = parentNode.firstChild; n; n = n.nextSibling )
1722:               if ( n.nodeType == 1 )
1723:                 n.nodeIndex = c++;
1724: 
1725:             merge[id] = true;
1726:           }
1727: 
1728:           var add = false;
1729: 
1730:           if ( first == 0 ) {
1731:             if ( node.nodeIndex == last )
1732:               add = true;
1733:           } else if ( (node.nodeIndex - last) % first == 0 && (node.nodeIndex - last) / first >= 0 )
1734:             add = true;
1735: 
1736:           if ( add ^ not )
1737:             tmp.push( node );
1738:         }
1739: 
1740:         r = tmp;
1741: 

1705行目からは、子要素フィルタのための処理で、:nth-child(2) のような書式を扱います。1708行目の正規表現は、'even', 'odd', '5', '2n', '3n+2', '4n-1', '-n+6'などの書式をパースして(first)n+(last)形式に変換し、firstおよびlastの数値をそれぞれ抽出します。'even'の場合は「2n⁠⁠、'odd'の場合は「2n+1」に変換してからパースします。

そして、1715行目のforループで要素を順に調べていきます。ここで出てくる変数mergeは、重複を無くすためのフラグです。1716行目でユニークなidを取得し、既に評価済みの要素の場合には、merge[id]をtrueにセットすることで二度同じ要素を評価しないようにしています。そして、1721行目のforループで子要素ノードを順にナンバリングしておきます。

1730行目ですが、firstが0の場合は、(first)n+(last)の式はlastと等しくなるので、先ほどナンバリングしたnodeIndex == lastならばaddフラグをtrueにしておきます。firstが0以外の場合は、(first)n+(last)の式を満たす場合にaddフラグをtrueに設定します。

1736行目にて、notがtrueかfalseかによってtmp配列に格納します。1740行目にてその配列をrに格納します。

1742:       // Otherwise, find the expression to execute
1743:       } else {
1744:         var f = jQuery.expr[m[1]];
1745:         if ( typeof f != "string" )
1746:           f = jQuery.expr[m[1]][m[2]];
1747: 
1748:         // Build a custom macro to enclose it
1749:         f = eval("false||function(a,i){return " + f + "}");
1750: 
1751:         // Execute it against the current filter
1752:         r = jQuery.grep( r, f, not );
1753:       }
1754:     }
1755: 
1756:     // Return an array of filtered elements (r)
1757:     // and the modified expression string (t)
1758:     return { r: r, t: t };
1759:   },
1760: 

以上のどのセレクタ式でもない場合ですが、jQuery.exprに該当するハッシュのキーを参照して、それが文字列でない場合はm[1]m[2]のキーを検索して変数 f に代入します。そして、1749行目にてeval()によりその式を返す関数を作成します。最後にjQuery.grep()メソッドによってその関数を要素配列に対して適用します。

最後に1758行目にて、r,tを持つハッシュ値を返して終了となります。

jQuery.dir()

1761:   dir: function( elem, dir ){
1762:     var matched = [];
1763:     var cur = elem[dir];
1764:     while ( cur && cur != document ) {
1765:       if ( cur.nodeType == 1 )
1766:         matched.push( cur );
1767:       cur = cur[dir];
1768:     }
1769:     return matched;
1770:   },
1771:   

jQuery.dirメソッドは、引数として指定されたelem要素のdirノードを順に辿っていき、要素の配列を返します。1763行目で、変数curに最初のelem[dir]の値を格納します。引数dirには"parentNode"や"nextSibling"などの値が渡されてくるので、1764行目のwhileループにて、目的のcur要素がなくなるまで、もしくはdocumentノードに到達するまで繰り返し処理を行い、matched配列に追加していきます。

jQuery.nth()

1772:   nth: function(cur,result,dir,elem){
1773:     result = result || 1;
1774:     var num = 0;
1775: 
1776:     for ( ; cur; cur = cur[dir] )
1777:       if ( cur.nodeType == 1 && ++num == result )
1778:         break;
1779: 
1780:     return cur;
1781:   },
1782:   

jQuery.nthメソッドは、cur要素からresult番目の要素を抽出します。dirには"nextSibling"や"previousSibling"などの値を指定して、指定された要素をどのように辿っていくかを指定します。例えば"nextSibling"が指定された場合には、curの兄弟要素を次々に辿っていくことになります。result番目の要素が見つかったら、1780行目でその要素を返します。

なお、第4引数elemは利用されていないようです。

jQuery.sibling()

1783:   sibling: function( n, elem ) {
1784:     var r = [];
1785: 
1786:     for ( ; n; n = n.nextSibling ) {
1787:       if ( n.nodeType == 1 && (!elem || n != elem) )
1788:         r.push( n );
1789:     }
1790: 
1791:     return r;
1792:   }
1793: });
1794: 

jQuery.siblingメソッドは、nの中から第2引数 elem 以外の兄弟要素を返します。1786行目のfor文で兄弟要素を順に辿っていき、elemが指定されている場合は、elem要素以外のものを配列にプッシュして返します。

おすすめ記事

記事・ニュース一覧