jquery.jsを読み解く

第6回jQueryライブラリ(1360行目~1636行目)

今回は、jQuery特有のセレクタ式を処理する部分になります。jQuery.filterメソッドについては次回の説明となりますが、そちらも合わせて読んで行くとより理解しやすいかと思います。

jQuery.expr

1360行目からは、セレクタ式のための正規表現を定義する部分です。

具体的な説明に入る前に、まずソースコード中に登場するm[2]とm[3]が何を表すのかを説明しておきましょう。mは1658行目にて定義されていて、jQuery.parseの正規表現にマッチした結果が格納されます。また、aには対象となる要素が格納されます。

1364行目からは、":"以降に続くフィルタの定義になります。細かな部分はjQueryドキュメントのAPI/1.2/Selectorsの部分を読んで頂ければ理解できると思うので、ここではいくつかの重要な箇所に絞って説明していきます。

1360: jQuery.extend({
1361:   expr: {
1362:     "": "m[2]=='*'||jQuery.nodeName(a,m[2])",
1363:     "#": "a.getAttribute('id')==m[2]",
1364:     ":": {
1365:       // Position Checks
1366:       lt: "i<m[3]-0",
1367:       gt: "i>m[3]-0",
1368:       nth: "m[3]-0==i",
1369:       eq: "m[3]-0==i",
1370:       first: "i==0",
1371:       last: "i==r.length-1",
1372:       even: "i%2==0",
1373:       odd: "i%2",
1374: 
1375:       // Child Checks
1376:       "first-child": "a.parentNode.getElementsByTagName('*')[0]==a",
1377:       "last-child": "jQuery.nth(a.parentNode.lastChild,1,'previousSibling')==a",
1378:       "only-child": "!jQuery.nth(a.parentNode.lastChild,2,'previousSibling')",
1379: 
1380:       // Parent Checks
1381:       parent: "a.firstChild",
1382:       empty: "!a.firstChild",
1383: 
1384:       // Text Check
1385:       contains: "(a.textContent||a.innerText||jQuery(a).text()||'').indexOf(m[3])>=0",
1386: 
1387:       // Visibility
1388:       visible: '"hidden"!=a.type&&jQuery.css(a,"display")!="none"&&jQuery.css(a,"visibility")!="hidden"',
1389:       hidden: '"hidden"==a.type||jQuery.css(a,"display")=="none"||jQuery.css(a,"visibility")=="hidden"',
1390: 
1391:       // Form attributes
1392:       enabled: "!a.disabled",
1393:       disabled: "a.disabled",
1394:       checked: "a.checked",
1395:       selected: "a.selected||jQuery.attr(a,'selected')",
1396: 
1397:       // Form elements
1398:       text: "'text'==a.type",
1399:       radio: "'radio'==a.type",
1400:       checkbox: "'checkbox'==a.type",
1401:       file: "'file'==a.type",
1402:       password: "'password'==a.type",
1403:       submit: "'submit'==a.type",
1404:       image: "'image'==a.type",
1405:       reset: "'reset'==a.type",
1406:       button: '"button"==a.type||jQuery.nodeName(a,"button")',
1407:       input: "/input|select|textarea|button/i.test(a.nodeName)",
1408: 
1409:       // :has()
1410:       has: "jQuery.find(m[3],a).length",
1411: 
1412:       // :header
1413:       header: "/h\\d/i.test(a.nodeName)",
1414: 
1415:       // :animated
1416:       animated: "jQuery.grep(jQuery.timers,function(fn){return a==fn.elem;}).length"
1417:     }
1418:   },
1419:   

1365行目に頻繁に現われるiはマッチした要素の中で何番目かを表すものです。例えば、evenを見ると2で割った余りが0だとtrueになるので、偶数番目の要素だとtrueになるという具合です。

1375行目からは親要素/子要素があるかどうか、テキストノードかどうか、可視要素かどうかを調べるためのものです。

1397行目からは、Form要素のタイプを判別するもので、対象要素のtypeがそれぞれ等しいときにtrueになります。同様にhas()はその要素がみつかればtrue、headerはh1、h2、…のヘッダ要素ならtrue、animatedはアニメーションが動作中の場合にtrueになります。

jQuery.parse(セレクタ式評価用の正規表現)

1420:   // The regular expressions that power the parsing engine
1421:   parse: [
1422:     // Match: [@value='test'], [@foo]
1423:     /^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,
1424: 
1425:     // Match: :contains('foo')
1426:     /^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,
1427: 
1428:     // Match: :even, :last-chlid, #id, .class
1429:     new RegExp("^([:.#]*)(" + chars + "+)")
1430:   ],
1431: 

1420行目からは、セレクタ式をパースするための正規表現を定義している部分です。filterメソッドで利用されます。

ここでは3つの正規表現が定義されていて、コメントにあるように1423行目は [@value='test'] または [@foo] のような属性に関するものを扱う場合に利用する正規表現です。また、1426行目は :contains('foo') のように()が付いた擬似セレクタを利用する正規表現です。最後に1429行目が :even, :last-child, #id, .class のような:#.から始まる擬似セレクタとidまたはクラス指定にマッチする正規表現です。

jQuery.multiFilter()

1432:   multiFilter: function( expr, elems, not ) {
1433:     var old, cur = [];
1434: 
1435:     while ( expr && expr != old ) {
1436:       old = expr;
1437:       var f = jQuery.filter( expr, elems, not );
1438:       expr = f.t.replace(/^\s*,\s*/, "" );
1439:       cur = not ? elems = f.r : jQuery.merge( cur, f.r );
1440:     }
1441: 
1442:     return cur;
1443:   },
1444: 

1432行目からのjQuery.multiFilter()は内部的に利用するためのメソッドです。次に説明するjQuery.filterメソッドを次々に呼び出していきます。f.tには処理した式は除外されて返ってくるので、'foo, bar'のようなセレクタ式を次々に処理していくことができます。

jQuery.find()

1445行目からのjQuery.findメソッドは、284行目で定義されているfindメソッドの核となる部分です。長いので、適当な箇所で区切って順に見ていきます。

1445:   find: function( t, context ) {
1446:     // Quickly handle non-string expressions
1447:     if ( typeof t != "string" )
1448:       return [ t ];
1449: 
1450:     // check to make sure context is a DOM element or a document
1451:     if ( context && context.nodeType != 1 && context.nodeType != 9)
1452:       return [ ];
1453: 
1454:     // Set the correct context (if none is provided)
1455:     context = context || document;
1456: 
1457:     // Initialize the search
1458:     var ret = [context], done = [], last, nodeName;
1459: 

1447行目ですが、第1引数が文字列型でない場合は、そのまま t を返します。次に1451行目で、第2引数contextの値をチェックします。contextのnodeTypeがエレメント(1)でもdocument(9)でもない場合は、空の配列を返します。そして、次の1455行目は、contextが与えられなかった場合にcontextの値としてdocumentを設定しています。最後に、1458行目で変数の値を初期化します。

1460:     // Continue while a selector expression exists, and while
1461:     // we're no longer looping upon ourselves
1462:     while ( t && last != t ) {
1463:       var r = [];
1464:       last = t;
1465: 
1466:       t = jQuery.trim(t);
1467: 
1468:       var foundToken = false;
1469: 

1464行目でlastの値として、第1引数で指定されたセレクタ式を設定しています。このセレクタ式内の文字列に対して繰り返し処理を行っていきます。順に処理をしていってlastの値が変化していくため、lastの値が変化していなかったら終了となります。foundTokenは、マッチする要素が見つかった場合にtrueになるフラグです。

1470:       // An attempt at speeding up child selectors that
1471:       // point to a specific element tag
1472:       var re = quickChild;
1473:       var m = re.exec(t);
1474: 
1475:       if ( m ) {
1476:         nodeName = m[1].toUpperCase();
1477: 
1478:         // Perform our own iteration and filter
1479:         for ( var i = 0; ret[i]; i++ )
1480:           for ( var c = ret[i].firstChild; c; c = c.nextSibling )
1481:             if ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) )
1482:               r.push( c );
1483: 
1484:         ret = r;
1485:         t = t.replace( re, "" );
1486:         if ( t.indexOf(" ") == 0 ) continue;
1487:         foundToken = true;

1470行目からは、quickChildにマッチするかをどうかをチェックし、マッチした場合の処理を行う部分です。quickChildについては1356行目で定義されていて、"> foo"のような子要素を選択する表元です。このような比較的簡単に処理することができるセレクタ式を切り分けることで、処理の高速化を実現しています。

まず、1476行目で大文字に正規化します。そして、検索対象contextの子要素を順に調べていって、要素ノードでセレクタ式が*または要素名と一致した場合に一時的にr配列に格納しています。ループ終了後に、結果をret配列に格納し、セレクタ式から今処理を行った部分を削除します。そして、セレクタ式の先頭に空白が含まれていてまだ続きがあれば処理を継続します。

最後に、foundTokenというみつかったかどうかのフラグをtrueに設定して終了です。

1488:       } else {
1489:         re = /^([>+)\s*(\w*)/i;
1490: 
1491:         if ( (m = re.exec(t)) != null ) {
1492:           r = [];
1493: 
1494:           var merge = {};
1495:           nodeName = m[2].toUpperCase();
1496:           m = m[1];
1497: 
1498:           for ( var j = 0, rl = ret.length; j < rl; j++ ) {
1499:             var n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild;
1500:             for ( ; n; n = n.nextSibling )
1501:               if ( n.nodeType == 1 ) {
1502:                 var id = jQuery.data(n);
1503: 
1504:                 if ( m == "~" && merge[id] ) break;
1505:                 
1506:                 if (!nodeName || n.nodeName.toUpperCase() == nodeName ) {
1507:                   if ( m == "~" ) merge[id] = true;
1508:                   r.push( n );
1509:                 }
1510:                 
1511:                 if ( m == "+" ) break;
1512:               }
1513:           }
1514: 
1515:           ret = r;
1516: 
1517:           // And remove the token
1518:           t = jQuery.trim( t.replace( re, "" ) );
1519:           foundToken = true;
1520:         }
1521:       }
1522: 

1488行目からは、セレクタ式が > + ~ のいずれかで始まる場合の処理です。ここで、これらのセレクタ式について少し説明しておくと、次のようになります。

  • '>'は、⁠A > B」とあった場合にAの子要素を選択するセレクタです。
  • '~'は、⁠A ~ B」とあった場合にAの後に続く兄弟要素を選択するセレクタです。
  • '+'は、⁠A + B」とあった場合にAの後に続くBを選択するセレクタです。

以上を踏まえた上で、ソースコードを見ていきましょう。

1489行目の正規表現で'> foo','~ foo','+ foo'のような文字列にマッチするかどうか判定し、もしみつかれば変数を初期化していきます。そして、1499行目で'~'または'+'の場合にnにnextSiblingを代入、'>'の場合はnにfirstChildを代入します。そのnについて順に調査していき、要素ノードであれば変数idにユニークな値を設定します。

1504行目では、'~'による兄弟要素の検索でmerge[id]がtrueならばループを抜けます。要素が見つかった場合には、r配列に値をプッシュします。'+'の場合は、最初の兄弟要素を調べるだけなので、1511行目でループを1回だけで終了します。先ほどと同様に、最後にret配列に結果を格納し、foundTokenのフラグをtrueに設定して終了です。

1523:       // See if there's still an expression, and that we haven't already
1524:       // matched a token
1525:       if ( t && !foundToken ) {
1526:         // Handle multiple expressions
1527:         if ( !t.indexOf(",") ) {
1528:           // Clean the result set
1529:           if ( context == ret[0] ) ret.shift();
1530: 
1531:           // Merge the result sets
1532:           done = jQuery.merge( done, ret );
1533: 
1534:           // Reset the context
1535:           r = ret = [context];
1536: 
1537:           // Touch up the selector string
1538:           t = " " + t.substr(1,t.length);
1539:

1523行目からは、まだ評価する式が残っていて、マッチした結果も見つかっていない場合の処理です。1527行目は、','が先頭にある場合、つまり複数のセレクタ式指定を処理する部分です。contextとret配列の最初の要素が等しい場合は、その要素を削除します。そして、ret配列をマージして最初の','をスペースに置き換えます。

1540:         } else {
1541:           // Optimize for the case nodeName#idName
1542:           var re2 = quickID;
1543:           var m = re2.exec(t);
1544:           
1545:           // Re-organize the results, so that they're consistent
1546:           if ( m ) {
1547:             m = [ 0, m[2], m[3], m[1] ];
1548: 
1549:           } else {
1550:             // Otherwise, do a traditional filter check for
1551:             // ID, class, and element selectors
1552:             re2 = quickClass;
1553:             m = re2.exec(t);
1554:           }
1555: 
1556:           m[2] = m[2].replace(/\\/g, "");
1557: 
1558:           var elem = ret[ret.length-1];
1559: 

1545行目からは、foo#bar形式の表示を処理します。この場合は、quickIDを使ってマッチすれば変数mを[ 0, "#", idName, nodeName ]のように変更します。マッチしなければ、quickClassを適用して変数mを設定します。最後にelemにret配列の最後の要素を設定します。

1560:           // Try to do a global search by ID, where we can
1561:           if ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
1562:             // Optimization for HTML document case
1563:             var oid = elem.getElementById(m[2]);
1564:             
1565:             // Do a quick check for the existence of the actual ID attribute
1566:             // to avoid selecting by the name attribute in IE
1567:             // also check to insure id is a string to avoid selecting an element with the name of 'id' inside a form
1568:             if ( (jQuery.browser.msie||jQuery.browser.opera) && oid && typeof oid.id == "string" && oid.id != m[2] )
1569:               oid = jQuery('[@id="'+m[2]+'"]', elem)[0];
1570: 
1571:             // Do a quick check for node name (where applicable) so
1572:             // that div#foo searches will be really fast
1573:             ret = r = oid && (!m[3] || jQuery.nodeName(oid, m[3])) ? [oid] : [];

1561行目からは、先ほど設定したelemを利用します。m[1]の値が#なら、getElementByIdを使って要素を取得します。ただし、IEとOperaで発生する問題を回避するためにjQuery()メソッドを使って、本当にid=m[2]を持つ要素かどうかを調べています。最後に1573行目で、要素名が本当に正しいかどうかをチェックします。

1574:           } else {
1575:             // We need to find all descendant elements
1576:             for ( var i = 0; ret[i]; i++ ) {
1577:               // Grab the tag name being searched for
1578:               var tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];
1579: 
1580:               // Handle IE7 being really dumb about s
1581:               if ( tag == "*" && ret[i].nodeName.toLowerCase() == "object" )
1582:                 tag = "param";
1583: 
1584:               r = jQuery.merge( r, ret[i].getElementsByTagName( tag ));
1585:             }
1586: 

1574行目からは、すべての子孫要素を検索する処理です。1581行目では、タグだったら、変数tagに'param'を設定しています。

1587:             // It's faster to filter by class and be done with it
1588:             if ( m[1] == "." )
1589:               r = jQuery.classFilter( r, m[2] );
1590: 
1591:             // Same with ID filtering
1592:             if ( m[1] == "#" ) {
1593:               var tmp = [];
1594: 
1595:               // Try to find the element with the ID
1596:               for ( var i = 0; r[i]; i++ )
1597:                 if ( r[i].getAttribute("id") == m[2] ) {
1598:                   tmp = [ r[i] ];
1599:                   break;
1600:                 }
1601: 
1602:               r = tmp;
1603:             }
1604: 
1605:             ret = r;
1606:           }
1607: 
1608:           t = t.replace( re2, "" );
1609:         }
1610: 
1611:       }
1612: 

1587行目は、jQuery.classFilter()メソッドを使って目的の要素を抽出しています。そして、id指定の場合でid名がm[2]と等しければtmp変数に設定してループを終了します。

最後に1608行目で検索対象の文字列を削除して終了です。

1613:       // If a selector string still exists
1614:       if ( t ) {
1615:         // Attempt to filter it
1616:         var val = jQuery.filter(t,r);
1617:         ret = r = val.r;
1618:         t = jQuery.trim(val.t);
1619:       }
1620:     }
1621: 

ここまで処理を行って、まだセレクタ文字列が残っている場合は、jQuery.filterメソッドを適用した結果を設定します。

1622:     // An error occurred with the selector;
1623:     // just return an empty set instead
1624:     if ( t )
1625:       ret = [];
1626: 
1627:     // Remove the root context
1628:     if ( ret && context == ret[0] )
1629:       ret.shift();
1630: 
1631:     // And combine the results
1632:     done = jQuery.merge( done, ret );
1633: 
1634:     return done;
1635:   },
1636: 

エラーが起こった場合は、代わりに空の配列を設定します。そして、1628行目でretの最初の要素がcontextと等しければ、それを削除します。最後にret配列をマージしてそれを返します。

おすすめ記事

記事・ニュース一覧