今回は、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:
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:
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:
1381: parent: "a.firstChild",
1382: empty: "!a.firstChild",
1383:
1384:
1385: contains: "(a.textContent||a.innerText||jQuery(a).text()||'').indexOf(m[3])>=0",
1386:
1387:
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:
1392: enabled: "!a.disabled",
1393: disabled: "a.disabled",
1394: checked: "a.checked",
1395: selected: "a.selected||jQuery.attr(a,'selected')",
1396:
1397:
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:
1410: has: "jQuery.find(m[3],a).length",
1411:
1412:
1413: header: "/h\\d/i.test(a.nodeName)",
1414:
1415:
1416: animated: "jQuery.grep(jQuery.timers,function (fn){ 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:
1421: parse: [
1422:
1423: /^(\[) *@?([\w-]+) *([!*$^~=]*) *('?"?)(.*?)\4 *\]/,
1424:
1425:
1426: /^(:)([\w-]+)\("?'?(.*?(\(.*?\))?[^(]*?)"?'?\)/,
1427:
1428:
1429: 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: old, cur = [];
1434:
1435: while ( expr && expr != old ) {
1436: old = expr;
1437: 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: 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:
1447: ( typeof t != "string" )
1448: [ t ];
1449:
1450:
1451: ( context && context.nodeType != 1 && context.nodeType != 9)
1452: [ ];
1453:
1454:
1455: context = context || document;
1456:
1457:
1458: ret = [context], done = [], last, nodeName;
1459:
1447行目ですが、第1引数が文字列型でない場合は、そのまま t を返します。次に1451行目で、第2引数contextの値をチェックします。contextのnodeTypeがエレメント(1)でもdocument(9)でもない場合は、空の配列を返します。そして、次の1455行目は、contextが与えられなかった場合にcontextの値としてdocumentを設定しています。最後に、1458行目で変数の値を初期化します。
1460:
1461:
1462: while ( t && last != t ) {
1463: r = [];
1464: last = t;
1465:
1466: t = jQuery.trim(t);
1467:
1468: foundToken = false;
1469:
1464行目でlastの値として、第1引数で指定されたセレクタ式を設定しています。このセレクタ式内の文字列に対して繰り返し処理を行っていきます。順に処理をしていってlastの値が変化していくため、lastの値が変化していなかったら終了となります。foundTokenは、マッチする要素が見つかった場合にtrueになるフラグです。
1470:
1471:
1472: re = quickChild;
1473: m = re.exec(t);
1474:
1475: ( m ) {
1476: nodeName = m[1].toUpperCase();
1477:
1478:
1479: for ( i = 0; ret[i]; i++ )
1480: for ( c = ret[i].firstChild; c; c = c.nextSibling )
1481: ( c.nodeType == 1 && (nodeName == "*" || c.nodeName.toUpperCase() == nodeName) )
1482: r.push( c );
1483:
1484: ret = r;
1485: t = t.replace( re, "" );
1486: ( t.indexOf(" ") == 0 ) continue;
1487: foundToken = true;
1470行目からは、quickChildにマッチするかをどうかをチェックし、マッチした場合の処理を行う部分です。quickChildについては1356行目で定義されていて、"> foo"のような子要素を選択する表元です。このような比較的簡単に処理することができるセレクタ式を切り分けることで、処理の高速化を実現しています。
まず、1476行目で大文字に正規化します。そして、検索対象contextの子要素を順に調べていって、要素ノードでセレクタ式が*または要素名と一致した場合に一時的にr配列に格納しています。ループ終了後に、結果をret配列に格納し、セレクタ式から今処理を行った部分を削除します。そして、セレクタ式の先頭に空白が含まれていてまだ続きがあれば処理を継続します。
最後に、foundTokenというみつかったかどうかのフラグをtrueに設定して終了です。
1488: } {
1489: re = /^([>+)\s*(\w*)/i;
1490:
1491: ( (m = re.exec(t)) != null ) {
1492: r = [];
1493:
1494: merge = {};
1495: nodeName = m[2].toUpperCase();
1496: m = m[1];
1497:
1498: for ( j = 0, rl = ret.length; j < rl; j++ ) {
1499: n = m == "~" || m == "+" ? ret[j].nextSibling : ret[j].firstChild;
1500: for ( ; n; n = n.nextSibling )
1501: ( n.nodeType == 1 ) {
1502: id = jQuery.data(n);
1503:
1504: ( m == "~" && merge[id] ) break;
1505:
1506: (!nodeName || n.nodeName.toUpperCase() == nodeName ) {
1507: ( m == "~" ) merge[id] = true;
1508: r.push( n );
1509: }
1510:
1511: ( m == "+" ) break;
1512: }
1513: }
1514:
1515: ret = r;
1516:
1517:
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:
1524:
1525: ( t && !foundToken ) {
1526:
1527: ( !t.indexOf(",") ) {
1528:
1529: ( context == ret[0] ) ret.shift();
1530:
1531:
1532: done = jQuery.merge( done, ret );
1533:
1534:
1535: r = ret = [context];
1536:
1537:
1538: t = " " + t.substr(1,t.length);
1539:
1523行目からは、まだ評価する式が残っていて、マッチした結果も見つかっていない場合の処理です。1527行目は、','が先頭にある場合、つまり複数のセレクタ式指定を処理する部分です。contextとret配列の最初の要素が等しい場合は、その要素を削除します。そして、ret配列をマージして最初の','をスペースに置き換えます。
1540: } {
1541:
1542: re2 = quickID;
1543: m = re2.exec(t);
1544:
1545:
1546: ( m ) {
1547: m = [ 0, m[2], m[3], m[1] ];
1548:
1549: } {
1550:
1551:
1552: re2 = quickClass;
1553: m = re2.exec(t);
1554: }
1555:
1556: m[2] = m[2].replace(/\\/g, "");
1557:
1558: elem = ret[ret.length-1];
1559:
1545行目からは、foo#bar形式の表示を処理します。この場合は、quickIDを使ってマッチすれば変数mを[ 0, "#", idName, nodeName ]のように変更します。マッチしなければ、quickClassを適用して変数mを設定します。最後にelemにret配列の最後の要素を設定します。
1560:
1561: ( m[1] == "#" && elem && elem.getElementById && !jQuery.isXMLDoc(elem) ) {
1562:
1563: oid = elem.getElementById(m[2]);
1564:
1565:
1566:
1567:
1568: ( (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:
1572:
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: } {
1575:
1576: for ( i = 0; ret[i]; i++ ) {
1577:
1578: tag = m[1] == "#" && m[3] ? m[3] : m[1] != "" || m[0] == "" ? "*" : m[2];
1579:
1580:
1581: ( 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:
1588: ( m[1] == "." )
1589: r = jQuery.classFilter( r, m[2] );
1590:
1591:
1592: ( m[1] == "#" ) {
1593: tmp = [];
1594:
1595:
1596: for ( i = 0; r[i]; i++ )
1597: ( 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:
1614: ( t ) {
1615:
1616: val = jQuery.filter(t,r);
1617: ret = r = val.r;
1618: t = jQuery.trim(val.t);
1619: }
1620: }
1621:
ここまで処理を行って、まだセレクタ文字列が残っている場合は、jQuery.filterメソッドを適用した結果を設定します。
1622:
1623:
1624: ( t )
1625: ret = [];
1626:
1627:
1628: ( ret && context == ret[0] )
1629: ret.shift();
1630:
1631:
1632: done = jQuery.merge( done, ret );
1633:
1634: done;
1635: },
1636:
エラーが起こった場合は、代わりに空の配列を設定します。そして、1628行目でretの最初の要素がcontextと等しければ、それを削除します。最後にret配列をマージしてそれを返します。