このレポジトリは昔からある「google.comとブラウザのアドレスバーに打ちこんでEnterを押すとき一体何が起きているのか?」という問いに答えるのものです。
一般的な回答とは違い、全ての部分についてできる限り深く答えます。
この作業はみなさんの協力によりで行なわれています。あなたも是非参加してください!まだまだ詳細が足りない部分が本当にたくさんあります。 そういった欠けた部分はあなたが追記してくれるのを待っているのです!プルリクエストを送ってください、お願いします!
ライセンスは全て Creative Commons Zero に従います。 中国語で読みたい方は 简体中文 から、韓国語は 한국어 からどうぞ。注: これらはalex/what-happens-whenのレポジトリのメンテナが内容をチェックしたわけではありません。
- "g"キーが押されると
- はじめは"Enter"キー
- 割り込み発火 (USBキーボード以外)
- (Windowsの場合)
WM_KEYDOWN
メッセージがアプリに送られます。 - (OS Xの場合)
KeyDown
NSEventがアプリに送られる - (GNU/Linuxの場合)Xorgサーバがキーコードをlistenする
- URLをパースする
- URLか?単語か?
- ホストネームに含まれるASCIIユニコード文字でない文字を変換する
- HSTSリストを調べる
- DNSルックアップ
- ARP処理
- ソケットを開く
- TLSハンドシェイク
- HTTPプロトコル
- HTTPサーバリクエスト処理
- ブラウザの裏側
- ブラウザ
- HTMLのパース
- CSSの解釈
- ページのレンダリング
- GPU レンダリング
- ウィンドウサーバー
- レンダリング後の処理および、ユーザの操作起因の処理
キーボードの物理的な動作とOSの割り込みについてです。"g"キーが押されるとブラウザはイベントを受けとり自動補完を行います。ブラウザのURLバーの下に、各ブラウザのアルゴリズムやプライベート/シークレットなどのモードによって異なる様々な候補がURLバーの下にドロップボックスとして表示されます。こういったアルゴリズムのほとんどは検索履歴、ブックマーク、クッキー、またインターネット全体での検索ワードの人気を利用して、結果の並び替えや優先順序付けを行います。"google.com"と入力するにつれたくさんのコードが実行されていき、1つのキーを打つごとに候補が正確になっていきます。入力が終わる前に"google.com"を候補にあげてくれることさえあるかもしれません。
キーボードのEnterキーを押すところから始めましょう。このタイミングででEnterキーの電気回路が(物理的にあるいは電気容量的に)閉じます。これによりキーボードの論理回路にわずかな電流が流れます。この論理回路は各キーのスイッチの状態を網羅的に調べ、素早く開閉するスイッチによる電気的ノイズを除き、キーコードの整数、今回の場合13に変換します。次にキーボードのコントローラーがキーコードをコンピュータに伝えるためエンコードします。これは現在ではほぼ必ずUniversal Serial Bus (USB)またはBluetooth connectionにより行われますが、歴史的にはPS/2またはADBコネクションにより行われてきました。
USBキーボードの場合:
- キーボードのUSB回路はコンピュータのUSBホストコントローラ(訳注: キーボード側にUSBデバイスコントローラがあり、対でUSBが機能する)のpin1によって供給される5Vの電気で動いています。
- 生成されたキーコードは、"endpoint"と呼ばれるキーボードのレジスタの回路記憶に保存されます。
- USBホストコントローラはその"endpoint"をおよそ10m秒ごと(キーボードにより指定された最小の値)にポーリングし、保存されたキーコードの値を取得します。
- この値はUSB SIE(Serial Interface Engine)へ伝わり、1つあるいは複数のUSBパケットに変換されます。ついでUSBの低レイヤープロトコルがこのパケットを処理します。
- このパケットはD+およびD-ピン(真ん中の2つ)の電気シグナルにより最大スピード(1.5Mb/s)で送信されます。なので、いつもHID (Human Interface Device)はUSB 2.0のコンプライアンスにより"low speed device"と言われています。
- この連番シグナルはコンピュータのUSBホストコントローラでデコードされ、Human Interface Device (HID)のキーボードデバイスドライバーによって解釈されます。ついでキーの値はOSのハードウェア抽象化レイヤーに渡されます。
仮想キーボードの場合(タッチスクリーンなど):
- ユーザが指で電気容量型のタッチスクリーンに触れると、ほんの小さな量の電流が指に流れます。この電流と導電層の静電場により回路が完成し、そのスクリーンのその点に電圧降下が生じます。
スクリーンコントローラ
は割り込みを発生させ、キーが押された軸を伝えます。 - 次にモバイルOSは現在開かれているアプリのGUI要素のどれかにプレスイベントを発生させます(今回の場合は仮想キーボードですが)。
- この仮想キーボードはソフトウェア割り込みを発生させ、「キーが押された」というメッセージをOSに伝えます。
- この割り込みが現在開かれているアプリに「キーが押された」というイベントを知らせます。
キーボードはシグナルをinterrupt request(IRQ:割り込み要求)を通じて送ります。IRQは 割り込みコントローラ
によって割り込みベクタ(整数)にマッピングされています。CPUは割り込みデスクリプタテーブル(Interrupt Descriptor Table:IDT)を利用して割り込みベクタをカーネルから提供される各機能(割り込みハンドラ
)にマッピングします。割り込みが起きると、CPUはIDTを割り込みベクタでインデクスし、対応するハンドラを実行します。カーネルにたどり着きました。
HIDトランスポートはkey downイベントを KBDHID.sys
ドライバに伝え、ついで KBDHID.sys
ドライバはHID usageをスキャンコードに変換します。この場合スキャンコードは VK_RETURN
( 0x0D
)です。 KBDHID.sys
ドライバは KBDCLASS.sys
(キーボードクラスドライバ)とやりとりを行います。このドライバは安全に全てのキーボードおよびキーパッドの入力を処理する責任があります。ついで Win32K.sys
(もしかすると外部からインストールされたサードバーティ製ののキーボードフィルタを経たあと)が動作します。これは全てカーネルモードで起こります。
Win32K.sys
は GetForegroundWindow()
APIを用いてどのウィンドウがアクティブかを判断します。このAPIによりブラウザのアドレスボックスのWindowハンドルが得られます。ついでWindowsの"message pump"がSendMessage(hWnd, WM_KEYDOWN, VK_RETURN, lParam)を呼びます。lParamはキーの押下に関するさらなる情報を示すビットマスクです。情報とはすなわちリピート回数(今回の場合は0)や実際のスキャンコード(OEMに依存しているかも知れませんが、一般にVK_RETURNの場合はOEM依存ではありません)、また他のalt, shift, ctrlなどが一緒に押されてたか、などの情報です。
Windowsの SendMessage
APIは特定のWindowハンドル( hWnd
)に対するキューにそのメッセージを追加する分かりやすい機能です。hWndに割り当てられた WindowProc
と呼ばれるメインのメッセージ処理機能が呼ばれて、キューに入ったメッセージは処理されていきます。
そのアクティブなウィンドウ( hWnd
)は実はエディットコントロールで、WindowProcはこの場合WM_KEYDOWNメッセージのためのメッセージハンドラを持ちます。このコードは SendMessage
タイミングで(wParam
)に渡された3番目の引見ます。今回はVK_RETURNなのでユーザがEnterキーを押したことが分かります。
割り込みシグナルがI/O Kit kextキーボードドライバに割り込みイベントを発生させます。このドライバは受け取ったシグナルをキーコードに変換してOS X WindowServerプロセスに渡します。最終的にWindowServerは適切な(例えばアクティブまたはリスニング状態の)アプリにMachポート経由でイベントをdispatchします。イベントはポートのイベントキューに入ります。イベントはmach_ipc_dispatchを実行できるだけの権限をもつスレッドによって読み込まれます。これは、NSEventType
が KeyDown
の NSEvent
を通じて NSApplication
メインイベントループにより最もよく起き、処理されます。
グラフィカルXサーバを利用する場合。Xサーバはキー入力を得るためgeneric event driver(evdev)を利用します。
キーコードからスキャンコードへのリマッピングはXサーバ特有のキーマップとルールで行われます。
押されたキーからスキャンコードへのマッピングが終わると、Xサーバはその文字をウィンドウマネジャー(DWM, metacity, i3など)へ送ります。 逆にウィンドウマネジャーは文字を対象のウィンドウへ送ります。
その文字を受け取ったウィンドウのグラフィカルAPIは、適切な文字を適切なフィールドに表示します。
ブラウザはURL(Uniform Resource Locator)から次の情報を得ることができます。
Protocol
"http"- プロトコルは"Hyper Text Transfer Protocol"を使う
Resource
"/"- メイン(インデックス)ページを取りに行く
プロトコルがない、あるいは有効なドメインでない場合、ブラウザは入力されたテキストをブラウザのデフォルトの検索エンジンに渡します。多くの場合、URLは特別なテキストが追加されるので、サーチエンジンはそのテキストがどのブラウザのURLバーから来たものなのかを知ることができます。
- ブラウザはホストネームの文字の中に「
a-z
,A-Z
,0-9
,-
,.
」以外の文字がないか調べます。 - 今回の場合ホストネームは"google.com"なのでそういった文字はありませんが、もしある場合にはURLのホストネーム部分に Punycode エンコーディングを適用します。
- ブラウザは"preloaded HSTS(HTTP Strict Transport Security)"リストを調べます。これはHTTPSでのみリクエストを送るように求めているウェブサイトの一覧です。
- もしそのウェブサイトがリストにあれば、ブラウザはHTTPではなくHTTPSでリクエストを送ります。なければ最初のリクエストはHTTPで送られます。ウェブサイトは、HSTS一覧になくてもHSTSポリシーを利用可能であることに注意してください。最初のHTTPリクエストに対するレスポンスは、HTTPSリクエストのみでリクエストを送ることを要求するものです。しかし、この1回のHTTPリクエストによりユーザはダウングレード攻撃を受ける可能性があります。そのため、現在のWebブラウザにはHSTS一覧が搭載されています。
- ブラウザは対象のドメインがキャッシュにないか調べます。(ChromeのDNSキャッシュを見たければ、 chrome://net-internals/#dns にアクセスしてください)
- もしキャッシュになければ、ブラウザは
gethostbyname
ライブラリ関数(OSにより異なる)を呼んで、ルックアップを行います。 gethostbyname
はホストネームの名前解決をするのに、DNSによる名前解決の前にローカルのホストファイル(OSにより場所は異なる)で解決できるか確認します。gethostbyname
がキャッシュに持っていなかったりhosts
ファイルにない場合は、ネットワークスタックで設定されたネットワークDNSサーバにリクエストを送ります。- 典型的なのは、ローカルのルーターかISPのキャッシュDNSサーバです。
- もしDNSサーバが同じサブネットにあれば、ネットワークライブラリはそのDNSサーバに対する
ARP処理
に従います。 - もしDNSサーバが異なるサブネットにあれば、ネットワークライブラリはデフォルトゲートウェイIPに対する
ARP処理
に従います。
ARP(Address Resolution Protocol)ブロードキャストを行うため、ネットワークスタックライブラリは対象のIPアドレスを知る必要があります。また、ARPブロードキャストを行うため、MACアドレスを知る必要もあります。
ARPキャッシュにARPエントリのターゲットIPがないか調べます。キャッシュにあれば、ライブラリは次のような結果を返します: Target IP = MAC
もしエントリーがARPキャッシュにない場合:
- ターゲットIPアドレスがローカルのルートテーブルのサブネットのいずれかにないかが調べられます。もしあればライブラリはそのサブネットのインターフェースを利用します。もしなければ、ライブラリはデフォルトゲートウェイのサブネットのインターフェースを利用します。
- 選択したネットワークインタフェースのMACアドレスを調べます。
- ネットワークライブラリはLayer2(OSI model におけるデータリンク層)にARPリクエストを送ります。
ARPリクエスト
:
送信者MAC: interface:mac:address:here 送信者IP: interface.ip.goes.here ターゲット MAC: FF:FF:FF:FF:FF:FF (ブロードキャスト) ターゲット IP: target.ip.goes.here
コンピュータとルータの間にあるハードウェアの種類によって以下のように変化します。
直接繋がれている場合:
- コンピュータがルータと直接接続されている場合、ルータはARPリプライを返します。
ハブの場合:
- コンピュータがハブに繋がっている場合、ハブはARPリクエストを他の全てのポートにブロードキャストします。もしルータが同じワイヤに繋がっている場合、ルータはARPリプライを返します。
スイッチの場合:
- コンピュータがスイッチに繋がっている場合、スイッチはローカルのCAM/MACテーブルからどのポートが探しているMACアドレスを持っているのか調べます。もしそのMACアドレスに対するエントリがなければ、他の全てのポートへARPリクエストをブロードキャストします。
- また、もしスイッチのMAC/CAMテーブルにそのMACアドレスがあれば、ARPリクエストをそのポートに送ります。
- また、もしルータが同じワイヤ上にあれば、ARPリプライを返します。
ARPリプライ
:
送信者MAC: target:mac:address:here 送信者IP: target.ip.goes.here ターゲットMAC: interface:mac:address:here ターゲットIP: interface.ip.goes.here
ネットワークライブラリが自分たちのDNSサーバあるいはデフォルトゲートウェイのIPアドレスを持っているので、DNSの処理を進めることができます。
- 53番ポートが開いて、DNSサーバにUDPリクエストを送ります(レスポンスサイズが大きすぎる場合は代わりにTCPが利用されます)。
- もしローカルまたはISPのDNSサーバがIPを知らなければ、再帰的探索がリクエストされて、一連のDNSサーバをたどり、SOAにたどり着き、もしあればAnswerが返されます。
ブラウザが目標サーバのIPを受け取ると、それとURLから得た適切なポート(HTTPは80, HTTPSは443)を用いて socket
という名前のシステム関数を呼び、TCPソケットストリーム( AF_INET/AF_INET6
と SOCK_STREAM
)をリクエストします。
- このリクエストははじめにTCPセグメントが生成されるトランスポートレイヤに渡されます。標的ポートがヘッダに追加され、ソースポートがカーネルの動的ポート幅(Linuxではip_local_port_range)から選ばれます。
- このセグメントはネットワークレイヤに送られIPヘッダが付与されます。標的サーバおよびクライアントののIPアドレスを利用してパケットが作られます。
- パケットはついでリンクレイヤに到着します。MACアドレスのゲートウェイ(ローカルルータ)およびNICのMacアドレスを含むフレームヘッダが付与されます。前と同じように、もしカーネルがゲートウェイのMACアドレスを知らない場合ARPリクエストを行なって探します。
この時点でパケットは以下のいずれかを通じてやりとりする準備ができています。
ほとんどの家庭用、あるいは小さなビジネス用のインターネットにおいてパケットはあなたのコンピュータから、場合によってはローカルネットワークを経由して、モデム(MOdulator/DEModulator)を通り、1と0のデジタルな情報を電話やケーブル、その他ワイヤレスな通信に適したアナログな形に変換します。コネクションの反対側では、別のモデムがそのアナログなデータをデジタルなデータに変換し、次の network node に渡されます。 ネットワークノードでは送信者および受信者のアドレスがより詳細に解析されます。
また大きな会社のほとんど、また新しい住宅のいくつかはファイバーかEthernetに直接つながっており、この場合データはデジタルのまま直接次の network node へと渡されます。
そしてパケットはローカルサブネットを管理するルーターにたどり着きます。ここから、AS(autonomous system)のボーダールーターや他のASに行き、最終的に標的のサーバにたどり着きます。移動経路上にあった各ルータはIPヘッダから標的サーバのアドレスを読み取り、適切な次のルータへと導きます。IPヘッダのTTL(time to live)フィールドはルータを1つ経るごとに1減ります。パケットはTTLが0に到達するか現在のルータのキューにスペースがないと、破棄されます。
この送受信は以下のTCPコネクションの流れの中で何回か行われます。
- クライアントはISN(initial sequence number : 初期連番番号)を決め、SYNビットをセットしてISNを設定しようとしていることを表しつつパケットをサーバに送ります。
- サーバはSYNを受け取ります。もし受け取り可能な場合、
- サーバは自身でISNを決めます。
- サーバはISNを選択しようとしていることを伝えるため、SYNをセットします。
- サーバはクライアントのISN+1の値を計算し、ACKフィールドに設定します。またACKフラグを設定して最初のパケットのリクエストを承認します。
- クライアントは以下のようなパケットを送ることでコネクションを承認します。
- 自身のシーケンス番号を増やす
- 受信者側のACK番号を増やす
- ACKフィールドを設定する
- データは以下のように通信されます
- 片側がNバイトのデータを送ると、SEQをその番号分増やします。
- もう片側がそのパケット(あるいは一連のパケット)を受け取ったことを確認するとACKパケットをACK値に最後に受け取ったシーケンス番号を入れて返します。
- コネクションを切る
- コネクションを切りたい側がFINパケットを送る
- もう一方はFINパケットをACKして、自分でもFINパケットを送信する
- コネクションを切りたかった側がACKパケットでFINをACKする。
- The client computer sends a
ClientHello
message to the server with its Transport Layer Security (TLS) version, list of cipher algorithms and compression methods available. - クライアントがClientHelloメッセージをTLSバージョン、可能な暗号化アルゴリズムおよび圧縮方法のリストと共にサーバに送ります。
- サーバはTLSのバージョン、選択した暗号化アルゴリズムおよび圧縮方法、CA(Certificate Authorityより署名された)サーバーの公開証明書と共に、ServerHelloメッセージでレスポンスを返します。
- クライアントはサーバの電子証明書を、信用しているCAのリストに照会します。サーバのCAが信用できるとなった場合、クライアントは擬似ランダムな文字列を生成してこれをサーバの公開鍵で暗号化します。このランダムな文字列は共通鍵として利用されます。
- サーバはプライベートキーで受け取ったランダム文字列を復号して、共通鍵を取得します。
- クライアントはここまでにあったやりとりのハッシュ値を公開鍵で暗号化して、
Finished
メッセージをサーバに送ります。 - サーバは自身でもハッシュを生成し、クライアントから送られてきたハッシュ値と比較します。もしあっていれば、サーバからも共通鍵で暗号化したFinishedメッセージをクライアントに送ります。
- これ以降は、TLSセッションによりアプリケーションのデータは共通鍵で暗号化されてやりとりされます。
もし利用しているウェブブラウザがGoogle製なら、ページを取得にはHTTPリクエストを送る代わりにHTTPからSPDYプロトコルにアップグレードするようなリクエストを送ります。
クライアントがHTTPプロトコルを使っていてかつSPDYをサポートしていない場合、ブラウザは以下の以下の形式で送ります。:
GET / HTTP/1.1 Host: google.com Connection: close [other headers]
[other headers]
はHTTP規約で定められた、いくつかのキーと値のペアで、ペア同士は改行で区切られます。(これはブラウザがHTTP規約を守り、HTTP/1.1を利用している場合に限ります。もしそうでければリクエストにHostヘッダーもないかもしれず、この場合バージョンはHTTP/1.0かHTTP/0.9が利用されます)
HTTP/1.1は送信者が"close"Connectionオプションをつけることができます。これをつけるとコネクションはレスポンスが返った後に閉じることを示唆します。例えば、
Connection: close
のようなものです。 接続を維持する機能をサポートしていないHTTP/1.1アプリケーションは必ず"close"コネクションオプションを全てのメッセージに含める必要があります。 HTTP/1.1 applications that do not support persistent connections MUST include the "close" connection option in every message.
リクエストとヘッダーを送った後はブラウザは改行文字1つだけを送り、サーバ側にリクエストが終わったことを伝えます。
サーバはリクエストの結果を表すレスポンスコードなどを以下のようなフォーマットで返します。
200 OK [レスポンス ヘッダ]
この次の改行文字のあと、www.google.comのHTML が続きます。次にサーバはコネクションを切るか、あるいはクライアントのリクエストヘッダによってはつなぎ続けてさらなるリクエストを待ちます。
ブラウザから送信されたHTTPヘッダから、ブラウザのファイルのキャッシュバージョン(ETagヘッダなど)を見て、最後に取得した時から変更がないことにサーバが気づいた場合、次のようなレスポンスを返すこともあります。
304 Not Modified [レスポンス ヘッダ]
それ以外の内容はなく、ブラウザはキャッシュからHTMLを取得することになります。 HTMLのパース後、ウェブブラウザ(およびサーバ)はこの処理をHTMLページから参照されるリソース(画像、CSS、ファビコンなど)ごとに繰り返します。
HTMLがwww.google.comと異なるドメインのリソースを参照していた場合、ウェブブラウザはそのドメインを名前解決するところまで戻ってそこから再開します。リクエストのHostヘッダはgoogle.comでなく別の適切な名前に設定されます。
サーバサイド側でリクエスト/レスポンスを処理しているのはHTTPD(HTTPデーモン)サーバです。1番一般的なHTTPDサーバはリナックスの場合Apacheかnginxで、Windowsの場合はIISです。
- HTTPDがリクエストを受け取ります。
- サーバはリクエストを分解して以下のパラメタをチェックします。
- HTTPリクエストメソッド(
GET
,HEAD
,POST
,PUT
,DELETE
,CONNECT
,OPTIONS
,TRACE
)。URLバーに直接打ち込んだ今回の場合、このパラメタはGET
になります。 - ドメイン。今回の場合はgoogle.com
- リクエストされたパス/ページ。今回の場合は何も指定されなかったのでデフォルトの'/'になります。
- HTTPリクエストメソッド(
- google.comに対するリクエスト用のバーチャルホストが設定されていることを確認します。
- また、サーバはgoogle.comがGETリクエストを受け取れることを確認します。
- さらにサーバはクライアントがこのメソッドを使って良いかを(IPや認証を通じて)確認します。
- Apacheのmod_rewriteやIISのURL RewriteのようなRewriteモジュールがサーバにあれば、リクエストと設定を比較します。もし対応する設定があれば、サーバはその設定にしたがってリクエストの書き換えを行います。
- サーバはリクエストに対応するコンテンツを用意します。今回の場合"/"なのでインデックスファイルです(この設定を上書きすることもできますが、これが最も一般的な方法です)。
- サーバはハンドラにしたがってファイルをパースします。もしGoogleがPHP上で動いていればサーバはPHPを利用してインデックスファイルを解釈し、クライアントに送ります。
サーバがHTMLやCSS、JS、画像などのリソースをブラウザに送ると、以下のようなことがおきています。
- HTML, CSS, JSをパース
- レンダリング - DOMツリーを構築 → ツリーをレンダー → レンダーツリーをレイアウト → レンダーツリーをペイント
ブラウザの役割は選択したWeb上のリソースをサーバからリクエストし、ブラウザの画面に表示することです。
リソースはHTMLドキュメントのことが多いですが、PDFや画像、またそれ以外かもしれません。 リソースの場所はURI(Uniform Resource Identifier)によって指定されます。
HTMLおよびCSSの規約にしたがってブラウザはHTMLを解釈し表示します。Webの標準化団体であるW3C(World Wide Web Consortium)により、これらの規約はメンテナンスされています。
各ブラウザのUIには多くの共通点があります。たとえば、
- URIを表示するアドレスバー
- 戻るボタンおよび進むボタン
- ブックマーク
- リロードボタンおよび現在のロードをやめるボタン
- ホームボタン
高レイヤから見たブラウザの構造
ブラウザの構成要素は:
- ユーザインターフェース ここでいうユーザインターフェスは、アドレスバーや戻る/進むボタン、ブックマークなどの、ブラウザのページ部分以外全てです。
- ブラウザエンジン ブラウザエンジンは、UIとレンダリングエンジン間の動きを制御するものです。
- レンダリングエンジン レンダリングエンジンはレスポンスの内容を表示します。たとえばレスポンスがHTMLならレンダリングエンジンはHTMLとCSSをパースして、スクリーンに表示します。
- ネットワーク ネットワークはHTTPリクエストなどのネットワークコールを、プラットフォーム間で共通のインターフェースを通じて行います。ただし、実装自体はプラットフォームにより異なります。
- UIバックエンド UIバックエンドはコンボボックスやウィンドウなどの基本的なウィジェットを表示するのに使います。このバックエンドはプラットフォームに依存しないインタフェースをもちます。裏側では、OSのユーザインタフェースメソッドを使っています。
- JavaScriptエンジン JavaScriptのコードをパースして実行します。
- DataStorage データストレージは記憶層にあたります。ブラウザはクッキーなどに様々なデータを保存できます。ブラウザは、localStorage, IndexedDB, WebSQL, FileSystemなどの保存方法をサポートしています。
まずレンダリングエンジンはネットワークレイヤーからコンテンツを取得します。通常、8kBのチャンク単位で行われます。
HTMLのパーサーの主な役割はHTMLマークアップを木構造(parse tree)にパースすることです。
出力された木("parse tree")は、DOM要素とアトリビュートをノードとする木です。ちなみにDOMはDocument Object Modelの略です。DOMはHTMLドキュメントのオブジェクト形式での表現であり、HTML要素のJSなどの外の世界に対するインターフェースでもあります。根は"Document"オブジェクトであり、スクリプトによる操作を行うまでDOMはマークアップと1対1の関係を持ちます。
パースアルゴリズム
HTMLは通常のトップダウン、あるいはボトムアップによるパースではうまくパースできません。
理由は次の通りです:
- HTMLは規則がゆるい
- ブラウザは伝統的に有名な無効なHTMLに対してはエラー耐性がある。
- パースの処理は"reentrant"。たとえば他の言語ではパースの最中に入力コードが変わることはないが、scriptタグに document.write() の呼び出しがあったりするとトークンが変化することになる。なので、パースの処理自体により入力が変化する。
上のような理由で通常のパース技術が使えないため、ブラウザはHTMLをパースするのに独自のパーサーを利用します。そのアルゴリズムはHTML5規約に詳細に記述されています。 アルゴリズムは大きく2つの段階からなります。トークン化と木構造の構築です。
パース終了時のアクション
ブラウザはリンクされた外部のリソース(CSS、画像、JSファイルなど)のフェッチを行います。
この段階でブラウザはドキュメントを操作可能なものとし、遅延評価モードのスクリプトのパースを開始します。遅延評価モードのスクリプトはドキュメントのパース後に実行されます。それが終わるとドキュメントの状態は"完了"状態になり、"ロード"イベントが発火します。
注意すべきなのはHTMLにおいて無効な文法はないというものです。ブラウザは内容の誤りを修正してパースを継続します。
- ”CSS lexical and syntax grammar”にもとづいてCSSファイル、styleタグの中身、styleアトリビュートをパースします。
- 各CSSファイルは"Stylesheet Object"にパースされます。スタイルシートオブジェクトとはセレクタやDOMオブジェクトと、対応するCSSルールをもったものです。
- CSSパーサーは様々ありますが、方式はトップダウンやボトムダウンで構いません。
- DOMノードをたどって'Frame Tree' または 'Render Tree'を作成し、各ノードのCSSスタイルの値を計算します。
- 'Frame Tree'の各ノードの幅を、子ノードの幅や左右のマージン、ボーダー、パディングを合計してボトムアップで計算します。
- 可能な幅を子ノードに割り当てていくことで、実際の幅をトップダウン式に決めていきます。
- 各ノードの高さをボトムアップで計算します。具体的にはテキストの折り返しや子ノードの高さ、自身のマージン、ボーダー、パディングを考慮に入れて合計します。
- 各ノードの座標を上までの計算結果から算出します。
- 要素が"フロート"だったり、positionが"absolute"や"relative"だったりすると、更に複雑な計算が行われます(http://dev.w3.org/csswg/css2/ や http://www.w3.org/Style/CSS/current-work を見てください)。
- ページのどの部分が"re-rasterized"せずにまとめてアニメーションできるかを示すレイヤーを作ります。各フレーム/レンダーオブジェクトはいずれかのレイヤーに割り当てられます。
- ページの各レイヤにはテクスチャが割り当てられます。
- 各レイヤのフレーム/レンダーオブジェクトはチェックされ、描画コマンドが対応するレイヤに対して実行されます。これはCPUによってラスタライズされるか、GPU(D2D/SkiaGL)によって直接描画されます。
- 上の全てのステップは最後に同じページがレンダーされて際に計算した値を再利用して、少しずつ変化するような変化の計算が簡単になるようにしています。
- ページのレイヤーは他のiframeやアドオンパネルなどのコンテンツと競合しないように計算されます。
- 最終的なレイヤーの位置が計算され、Direct3D/OpenGLによって複合コマンドが発行されます。GPUコマンドのバッファは非同期的なレンダリングをするためにGPUが担い、フレームはウィンドウサーバーに送られます。
- 画像計算レイヤはレンダリングの際の計算に、汎用的プロセッサである
CPU
や画像専用プロセッサであるGPU
を利用します。 GPU
を画像レンダリング計算に使う場合、画像のソフトウェアレイヤはタスクを小さく分割します。これによりGPU
の強力な並列処理能力をレンダリングに必要な浮動小数点計算に対して有効に使えます。
レンダリングが終了すると、ブラウザはJavaScriptを(Google Doodleアニメーションのように)時間差で実行したり、(検索ボックスに文字を入れると候補が出るように)ユーザの操作によって実行します。 FlashやJavaなどのプラグインも実行されるかもしれませんが、Googleのホーム画面の場合はなにもおきません。スクリプトによりネットワークリクエストが送られたり、ページの一部やレイアウトが変化して新たなページレンダリングや描画が行われるかもしれません。