Web●●○○○

DOM

どむ

HTMLをプログラムから操作できる木構造として表現したもの。動的なWebページの土台。

概要

DOM(Document Object Model)は、ブラウザが HTML を読み込んだときにメモリ上へ組み立てる、文書の「木構造のオブジェクト表現」です。HTMLはただの文字列ですが、ブラウザはそれを解析し、タグの入れ子関係をそのまま反映した木(ツリー)を作ります。この木の一つひとつの節(ノード)がオブジェクトになっていて、JavaScript から読み取ったり書き換えたりできる — これがDOMです。

「ボタンを押したら文言が変わる」「入力ミスで枠が赤くなる」といった動きのあるWebページは、すべてJavaScriptがDOMを書き換えることで実現されています。document.querySelector で要素を探し、textContentclassList を書き換える — フロントエンド開発の操作は、突き詰めればすべてこの木への読み書きです。

DOMは「いま表示されている画面の生きたモデル」でもあります。DOMを書き換えればブラウザは画面を描き直し、逆に開発者ツールで見える要素ツリーはDOMそのものです。HTMLが設計図だとすれば、DOMはその設計図から組み立てられ、あとから自由に増改築できる建物に当たります。

なぜ生まれたか

初期のWebページは、サーバから受け取ったHTMLをそのまま表示するだけの静的な文書でした。1995年にJavaScriptが登場し「読み込んだ後にページの中身を動かしたい」という需要が生まれると、Netscape Navigator と Internet Explorer はそれぞれ独自の方法でページ要素へアクセスする仕組みを実装します。同じ表現を作るのにブラウザごとに別々のコードを書かなければならない、いわゆる「DHTML時代」の互換性地獄です。

この混乱を収拾するため、W3Cは1998年に DOM Level 1 として「文書を木構造のオブジェクトとして表し、共通のAPIで操作する」標準を定めました。文書=木=オブジェクトという共通モデルが確立したことで、どのブラウザでも同じコードで文書を操作できるようになり、Ajax、そして SPA へと続く動的なWebの進化の土台になりました。

詳細

HTMLの解析からDOMツリーへ

ブラウザはHTMLを上から順に解析(パース)し、開きタグを見つけるたびにノードを作って親子関係をつなぎ、木を育てていきます。タグの入れ子が深さに、タグの並びが兄弟関係に対応するため、HTMLの構造とDOMツリーの形は一対一に対応します。重要なのは、タグに挟まれた文字列も「テキストノード」という独立したノードになることです。

HTML 文字列<html> <body> <h1>星図</h1> <p>本文…</p> </body>解析documenthtmlbodyh1p星図本文…テキストノード
HTML文字列の解析結果としてのDOMツリー — 入れ子が親子関係になる

ノードの種類

木の根は文書全体を表す document ノードで、その下に要素ノード(タグに対応)、テキストノード(文字列)、コメントノードなどがぶら下がります。要素ノードは idclass などの属性を持ち、parentNodechildren で木構造をたどれます。なお、ブラウザは CSS も同様に解析して CSSOM というオブジェクトモデルを作り、DOMと合成して画面を描画します。この描画の仕組み全体はレンダリングの語彙で扱います。

DOM操作APIと再描画コスト

document.querySelector / createElement / appendChild / remove といったAPIを使えば、要素の検索・生成・挿入・削除が自由にできます。ただし、DOMの書き換えはタダではありません。要素の大きさや位置に影響する変更は、ブラウザにレイアウトの再計算(リフロー)と再描画(リペイント)を強いるため、JavaScriptのオブジェクト操作に比べて桁違いに重い処理になります。

特に注意したいのが、ループの中で「書き換えては offsetHeight などのレイアウト値を読む」を繰り返すパターンで、読むたびに強制的なレイアウト再計算が走ります(レイアウトスラッシング)。書き換えはまとめて行う、DocumentFragment で組み立ててから一度に挿入する、といった工夫が古典的な対策です。この「DOM操作は重く、どこを最小限書き換えるかの管理が大変」という問題こそが、後の仮想DOMリアクティビティという発明の出発点になりました。

イベント伝播 — キャプチャとバブリング

ユーザーの操作はDOMツリーの上を「伝播」します。ボタンをクリックすると、イベントはまず document から木を下ってターゲット要素に到達し(キャプチャ段階)、その後こんどは木を上って document まで戻ります(バブリング段階)。addEventListener のリスナーは既定ではバブリング段階で呼ばれます。

ボタン親要素documentボタン親要素documentターゲット段階でリスナー実行キャプチャ段階で木を下るターゲットに到達バブリング段階で木を上るdocumentまで戻る

この仕組みを活かすと、リスト項目が1000個あっても各項目にリスナーを付けず、親要素に1つだけ付けてバブリングしてきたイベントの発生元を見分ける「イベントデリゲーション」が使えます。逆に、意図しない親のリスナーが反応してしまうときは stopPropagation で伝播を止めます。DOMが木であることは、表示構造だけでなくイベントの流れ方まで規定しているのです。