HTML5 × CSS3 × jQueryを真面目に勉強 – #7 jQueryのセレクタAPIについて詳しく
そんな訳で、普段何気なく使っているjQueryですが、そのセレクタAPIについて多少は知っておいたほうが良いよなということで、いくらか調べてみました。けっこう地味な内容なので、「へー、そんな風になってるんだぁ…」と軽く読み流していただければと思います嘘です。割と大事な内容なので、しっかりと把握しておくのがよろしいかと思います。
はじめに - jQueryのセレクタAPI
jQueryでは、$('#hoge .fuga');というようにCSSのセレクタを用いてHTML要素を取得します。あまりにも便利な機能で普段意識することはありませんが、内部ではgetElementById();といったブラウザのネイティブAPIを駆使したり、JavaScriptゴリゴリのメソッドを呼びまくって指定どおりの要素を取得してきているわけです。こういった機能のことをセレクタAPIと呼びます。
セレクタAPIの内訳
jQueryのセレクタAPIは、主に以下のような感じで分類することが出来ます。
- ブラウザのネイティブ機能
- getElements 系
- querySelectorAll
- Sizzle
ブラウザのネイティブ機能
皆さんがJavaScriptを習得する際に、早い段階で覚えるであろうgetElementById()やgetElementsClassName()といったAPIの事を指します。
- getElementById(selector)
- getElementsByClassName(selector) ※1
- getElementsByName(selector)
- getElementsByTagName(selector)
- querySelectorAll(selectors) ※次項にて解説
※1 ちなみにIE8はgetElementsByClassName();をサポートしていません。IE9よりサポートされています。
W3C DOM Compatibility - Core
querySelectorAll()
querySelectorAll()とは、jQueryと同じような書式のCSSセレクタで要素を指定することが可能なブラウザのネイティブAPIで、比較的新しくサポートされました。
querySelectorAll()は渡されたCSSセレクタにマッチした要素全てを配列で返し、類似のAPIであるquerySelector()は最初にマッチした要素だけを返します。
// #hoge .fugaに最初に該当する要素のみを返します document.querySelector('#hoge .fuga'); // #hoge .fugaに最初に該当する要素すべてを配列形式で返します document.querySelectorAll('#hoge .fuga');
サポートするブラウザは以下のとおり。
Internet Explorer | Firefox | Chrome | Safari | Opera | Android | iOS |
---|---|---|---|---|---|---|
8 ~ | 3.5 ~ | 1 ~ | 3.2 ~ | 10 ~ | 2.1 ~ | 3.2 ~ |
Sizzle
Sizzleとは、CSSセレクタを受け取ってそれに該当する要素を返すセレクターエンジンのことで、jQuery開発者であるJohn Resig氏によって作られたJavaScriptライブラリです。jQuery 1.3より実装されました。その後jQueryのバージョンアップとともに何度かのパフォーマンスチューニングが施されており、jQuery 1.8においても大規模なテコ入れがなされました。
querySelectorAll()がサポートされていないブラウザ用にCSSセレクタを実現するためのエンジンというのが当初の位置づけでしたが、querySelectorAll()だけではカバーしきれないjQuery独自のセレクタにも対応するための機能ということで、現在まで継続して実装されています。
jQuery セレクタ構文 | 選択対象 |
---|---|
:first | 最初に選択された要素 |
:last | 最後に選択された要素 |
:even | 偶数要素(インデックスは0スタート) |
:odd | 奇数要素(インデックスは0スタート) |
:eq(n) | インデックス(n)を元に1つの要素る |
:lt(n) | インデックスがnよりも小さい全ての要素 |
:gt(n) | インデックスがnよりも大きい全ての要素 |
:animated | アニメーションが開始されている要素 |
:visible | 可視状態となっている要素 |
:input | input、textarea、button、select要素 |
:password | <input type="password" /> |
:checkbox | <input type="checkbox" /> |
:radio | <input type="radio" /> |
:hidden | <input type="hidden" /> |
これらがひとつでもセレクタに含まれていると、Sizzleが呼び出されます。
jQueryが実際に呼び出すセレクタAPI
jQueryに渡されたセレクタに応じて内部でAPIが呼び出されるわけですが、どのAPIが呼び出されるかは、以下の優先度に従って決定します。
- 単一のID名やCLASS名の場合は、getElementById()やgetElementsByClassName()といった高速APIを使用
- (1)では処理しきれないが、querySelectorAll()が正常に通る場合は、これを使用
- (2)でも処理しきれない場合はSizzleを使用
ちなみにIE~7といったquerySelectorAll()をサポートしていないブラウザの場合は、(1)が通らなかった時点でSizzleが使用されます。
各セレクタAPIのベンチマークをとってみた
理屈だけ押し並べてもアレなので、即席のサンプルを作ってどれくらい速さに差がでるのか計測してみました。
- 実行環境
- OS | Windows 7 64bit
- CPU | Intel Core2 Quad Q6600
- RAM | 4.00GB
- ブラウザ | Chrome 22.0.x
- 計測内容
- jQueryとブラウザネイティブによるセレクタAPI呼び出しを1万回ずつ実行するのに要する時間を計測
では順番に見ていきます。
$('div#hoge'); // 131ms
$('div#hoge');と指定することによって、jQuery内部ではquerySelectorAll();が呼び出されました。遅いです。
$('#hoge'); // 24ms
$('#hoge');と指定することによって、jQuery内部ではgetElementById();が呼び出されました。速いです。
$('div.fuga'); // 119ms
$('div.fuga');と指定することによって、jQuery内部ではquerySelectorAll();が呼び出されました。遅いです。
$('.fuga'); //87ms
$('.fuga');と指定することによって、jQuery内部ではgetElementsByClassName();が呼び出されました。けっこう速いです。
$('#hoge:first'); // 606ms
$('#hoge:first');と指定することによって、jQuery内部ではSizzleが呼び出されました。がんばってるのは分かりますが、やっぱり遅いです。
続いてはjQueryを介さずに直接JavaScriptにてブラウザのネイティブAPIを呼び出した場合の結果ですが、
getElementById('hoge'); // 1ms getElementsByClassName('fuga'); // 3ms querySelectorAll('#hoge'); // 14ms querySelectorAll('.fuga'); // 21ms
すごく・・・、速いです・・・。
僕自身、実際に計測するまでは何となく速いんだろうなぁ、という程度にしか思っていませんでしたが、ここまで差が出るとは予想外でした。
補足 - コンテキストパラメータを使用した際のベンチマーク
$()には、第二引数にコンテキストを渡すことで、セレクタに指定された要素を検索する範囲をします。デフォルトはdocumentとなっており、DOM階層全体から検索されます。
http://s3pw.com/qrefy/jquery/
そんな訳で、コンテキストパラメータを指定した場合のベンチマークをとってみて、それに対する最適化の方法をメモっておくとします。
// コンテキストパラメータを使用 $('p', '#hoge'); // 155ms
コンテキストを指定するとjQUery内部でパラメータを解析し、コードを最適な式に変換した後に処理されます。なので遅いです。
// コードを最適化 $('#hoge').find('p'); // 116ms
jQuery内部でコードが自動変換されると書きましたが、その変換結果が正にこのコードであるため、最初から変換後のコードを記述することで、僅かながらパフォーマンスが向上しているのが分かります。
以上のことから、コンテキストパラメータを使用するくらいならば、findメソッドを活用するほうがSizzleの呼び出しや余計な処理をせずに済む品質の高いコードが書けるということが言えます。
おわりに
以上のことからjQueryから呼ばれたセレクタAPIの結果は、たとえ内部でネイティブAPIが実行されていたとしても、純粋にgetElementById()を実行した結果と比較してパフォーマンスに差ががあるのが証明されました。Sizzleに至ってはどうしても複雑な処理を行う以上、超えられない壁というのが出てきてしまうのは仕方ないね。
ただ誤解してはいけないのが、jQueryイラネというわけでは決してありません。むしろあってくれないと僕が一番困ります。Sizzleなしに複雑な条件の要素を取得するとなると、可読性の悪いコードが出来上がるだけで、何一つ良いことはありません。
今回の考察はjQueryをより深く理解する為に必要なステップであり、これを踏まえることで可読性およびパフォーマンス共に、より効率性の高いコードが書けることは間違いないはずです。