この記事はun-T factory! XA Advent Calendar 2021の16日目の記事です。
#この記事で伝えたいこと
- querySelectorAllとgetElementsBy~系の取得できるオブジェクトの違い、またその振る舞い
- 処理の早さ(重さ)の比較
#querySelectorAllとgetElementsBy~
2年のエンジニア生活で数えきれないほど使用したquerySelector(All)。
便利だからーというより、初めて見たjsのコードがこれで、以降盲信的に使い続けてきました。
反面、
2年のエンジニア生活でquerySelector(All)以外を使用したことが**あったようななかったような...**というレベルで、それ以外を知らなすぎる。
今後、よりquerySelector(All)をご機嫌に使えるように一旦立ち止まり掘り下げていきたいと思い立った次第です。
##querySelectorAllが便利すぎた
MDNから
selectors
DOMString で、照合対象となる 1 つまたは複数のセレクターを含みます。この文字列は妥当な CSS セレクターでなければならず、そうでない場合は SyntaxError 例外が発生します。複数のセレクターは、カンマで区切って指定することができます。
CSSセレクタで指定ができるので直感的でとてもわかりやすいquerySelectorAll、とても好きです。複雑な指定であってもcssの知識が一定あれば楽に取得ができるので、敷居が高くないのが利点ですね。
特定のクラス以下の要素を取得したりandやorの検索も可能。
また、擬似クラスもサポートされているので、:hoverや:activeなども使える。
(参考:[セレクターを使用した DOM 要素の特定]
(https://developer.mozilla.org/ja/docs/Web/API/Document_object_model/Locating_DOM_elements_using_selectors))
「bbbのクラスを持っていないaaa達を取得したい...」
「xxxの子要素のaaaとbbbを持つ要素をすべて取得」
が、一人で完結できるイカしたやつですね。
See the Pen Untitled by arichel (@arichel_unt) on CodePen.
##getElementsBy~を知る
盲信的querySelectorAllの卒業にあたっての代替案としてgetElementsBy~を検討したいと思います。
###getElementById
document.getElementById(id);
//取得できるオブジェクト:HTMLElementもしくはnull
idは必ず単一の要素であるため配列で返ることはないが、html内に同一のidがある場合も一応最初の要素は返すので、(nullにならないので)htmlでのidの重複には注意。
ちなみに、getElementById以外はgetElement(s)By~ とsが必要なので忘れないようにしたい。
idは必ず1つしか取得できないのでgetElementByIdとなります。
###getElementsByTagName
document.getElementsByTagName(name);
//取得できるオブジェクト:HTMLCollection(WebKitブラウザではNodeList)
タグ名を入れて取得。
例え1つの場合であってもHTMLCollectionで返すので、[0]や.item(0)で1つ目の指定が必要。
###getElementsByClassName
document.getElementsByClassName(class);
//取得できるオブジェクト:HTMLCollection
クラス名を入れて取得。
例え1つの場合であってもHTMLCollectionで返すので、[0]や.item(0)で1つ目の指定が必要。
##取得できるオブジェクトの違いで考える
メソッド | 取得できるオブジェクト |
---|---|
querySelector() | HTMLElement |
querySelectorAll() | NodeList |
getElementById() | HTMLElement |
getElementsByTagName() | HTMLCollection |
getElementsByClassName() | HTMLCollection |
Node.childNodesで取得できるNodeListは動的ですが、
querySelectorAllで取得できるのは静的なNodeListになります。
動的な NodeListは、Webページを構成するNodeをいわば参照している状態でありdocumentの変更が即時反映されます。
一方静的なNodeListは、取得したタイミングのNodeを複製した状態であり、documentに変更が加わろうと参照していないため影響を受けません。
また、HTMLCollectionは常に動的でありdocumentが変更された時点で自動的に更新されます。
つまり、
querySelectorAllで取得できる静的なNodeListと、
getElementsByTagNameとgetElementsByClassNameで取得できる動的なHTMLCollection
では、「動的」と「静的」という大きな違いがあります。
###HTMLCollectionと静的なNodeList
動的はしばしば生きて (live) なんて書かれ方をしていますね。静的は逆にライブじゃない、死んでいる、みたいな。
参照しているか複製しているかの違いに注意が必要です。
<div class="querySelectorAllWrap">
<div class="querySelectorAll">01</div>
<div class="querySelectorAll">02</div>
<div class="querySelectorAll">03</div>
<div class="querySelectorAll">04</div>
<div class="querySelectorAll">05</div>
</div>
<div class="getElementsByClassNameWrap">
<div class="getElementsByClassName">01</div>
<div class="getElementsByClassName">02</div>
<div class="getElementsByClassName">03</div>
<div class="getElementsByClassName">04</div>
<div class="getElementsByClassName">05</div>
</div>
const querySelectorAll = document.querySelectorAll('.querySelectorAll');
const querySelectorAllWrap = document.querySelector('.querySelectorAllWrap');
const getElementsByClassName = document.getElementsByClassName('getElementsByClassName');
const getElementsByClassNameWrap = document.getElementsByClassName('getElementsByClassNameWrap')[0];
console.log(querySelectorAll.length);
//...取得時点でのlengthは5
querySelectorAllWrap.insertAdjacentHTML('beforeend','<div class="querySelectorAll">06</div>' );
//...要素を1つ足しても...
console.log(querySelectorAll.length);
//...複製しているので変化はなく5のまま
console.log(getElementsByClassName.length);
//...取得時点でのlengthは5
getElementsByClassNameWrap.insertAdjacentHTML('beforeend','<div class="getElementsByClassName">06</div>' );
//...要素を1つ足すと...
console.log(getElementsByClassName.length);
//...参照しているので6になる
動的であるHTMLCollectionは、ページを表現するDOMにリアルタイムに接続したオブジェクトであり、経由して行われた変更は即時反映されます。
動的ではないNodeListは生成した段階のDOMがコピーされるイメージで、データを変更してもほかのNodeListには影響を与えず完全に独立している状態です。
動的:Webページを構成するNodeを参照(シャローコピー)
静的:Webページを構成するNodeを複製(ディープコピー)
また、速度の影響もあり、
静的なNodeListは最初からすべてのデータを保持する必要があるのに対し、HTMLCollectionはすべての情報を事前に保持する必要がないため、ブラウザでより速く作成して返すことができます。
##速度で考える
DOM要素1つを10000回取得するのにかかった時間を計測してみたいと思います。
用意したコードはこちら。
(平均値を取るとよいのですが次回とします。)
<div id="id" class="class">item</div>
const entrys = [
{
name: 'querySelector',
func: () => document.querySelector('.class')
},
{
name: 'querySelectorAll',
func: () => document.querySelectorAll('.class')
},
{
name: 'getElementById',
func: () => document.getElementById('id')
},
{
name: 'getElementsByTagName',
func: () => document.getElementsByTagName('div')
},
{
name: 'getElementsByClassName',
func: () => document.getElementsByClassName('class')
},
];
for (let i = 0; i < entrys.length; i++) {
console.time(entrys[i].name);
for (let n = 0; n < 10000; n++) {
entrys[i].func();
}
console.timeEnd(entrys[i].name);
}
結果は...
メソッド | 時間/ms | 倍率 |
---|---|---|
getElementsByClassName | 1.18115234375 ms | 1 |
getElementById | 1.527099609375 ms | 1.29 |
getElementsByTagName | 1.55908203125 ms | 1.32 |
querySelector(id) | 2.656005859375 ms | 2.25 |
querySelector(class) | 7.383056640625 ms | 6.25 |
querySelectorAll | 9.1640625 ms | 7.76 |
getElementByIdよりgetElementsByClassNameが早いのが意外でしたが、
querySelector(class)やquerySelectorAll(class)はgetElementsByClassNameの約6~7倍処理が遅いですね。
一つの目安としては、単純なクラスの指定であればgetElementsByClassNameを優先すべきかもしれません。
#まとめ
- 取得できるオブジェクトが違う。動的か静的かで取り扱いが変わる
- 複雑な指定はcssセレクタが扱えるquerySelectorAllが楽。classひとつならgetElementsByClassNameがよさそう
- querySelector(class)やquerySelectorAll(class)は処理が遅い
今回getElementsBy~を紐解き、漠然と使用していたメソッドの認識が深まり良かったですね。
「動けばいいや」から少しでも精度を上げていけたらと思います。
ありがとうございました。