27. ブラウザ API — document.getElementById と textContent で DOM を書き換え、描き直す
JavaScript が「ブラウザの言語」になるのは、 という名前で に触れるからです。この実装では 3 つだけ用意しました。document.getElementById(id)、.textContent の読み書き、console.log(...)。これらは JavaScript の文法ではなく、器が特定のオブジェクトと名の組に出会ったときに Rust 側で実行する組み込み(Builtin)です。
MemberExpression の評価で、オブジェクトが Document で名前が なら Builtin(GetElementById) を返し、CallExpression がそれを呼ぶと DOM Standard の手順どおり「子孫のうち で最初の、ID が一致する要素。無ければ null」を返します。要素の を読めば子孫のテキストをつないだもの、代入すれば「子を全部外して Text を 1 つ付ける」(string replace all)。この setter は E 章の set_text_content そのものです。
DOM を書き換えても、画面は自動では変わりません。とは DOM から作った別の木だからです。参照実装の は、script を実行したあとに ()(箱の木を作り直し、位置を決め直し、表示リストを作り直す)を呼びます。下のコードの実行結果は 3 段で、読み込み直後(script が count を 3 に)、あとから別の script で DOM を変えた直後(表示は古いまま)、relayout() 後(表示が追いつく)、と並んでいます。本物のブラウザはこの relayout を、JavaScript の実行が終わって次のフレームを描く前に自動で挟みます。
script の実行タイミングは本物と違います。本物はが <script> に出会った時点で解析を止めて実行します(だからその時点では後ろの要素はまだ DOM に無く、getElementById が null を返す)。この実装は DOM を全部作ってから実行するので、defer 付きの script に近い振る舞いです。script のエラーは記録しては続けます。これは本物と同じです。
本物のブラウザでは: document.getElementById のような『JS から C++ の DOM を呼ぶ橋』は手で書かず、 から生成します。 は core/dom/document.idl や xml_http_request.idl を Python の IDL コンパイラと Jinja テンプレートで C++ のにし(bindings/README.md)、JS オブジェクトは C++ オブジェクトの『』として寿命を の GC で管理します。Firefox は dom/webidl/*.webidl を dom/bindings/Codegen.py が生成します。この回の ConsoleLog と GetElementById の 2 つは、その生成物を 2 関数だけ手書きしたものです。実際の は Blink の LocalDOMWindow(core/frame/local_dom_window.cc)、Firefox の nsGlobalWindowInner です。
開発のどこに活かすか: 「DOM を触っても描画は別の段で、まとめて起きる」ことが、フレームワークが差分をまとめて反映する理由(React の再描画の単位、Angular の変更検知)と、レイアウトスラッシング(読み書きを交互に繰り返して毎回レイアウトを強制する)が遅い理由です。getElementById が null を返す古典的な不具合(script が要素より前にある)も、実行タイミングの違いとして説明できます。
点線の付いた語はタップすると説明が出ます。用語集を開く →
サンプルコード(フレームワーク環境が必要なため表示のみ)
// src/js/runtime.rs のブラウザ API の部分(MemberExpression と AssignmentExpression の評価)
Node::MemberExpression { object, property } => {
Node::AssignmentExpression { target, value } => {
// src/browser.rs(1 ページ分をつなぐ)
impl Page {
pub fn new() -> Self {
Page::default()
}
/// HTML 文字列を読み込んで、描画の準備まで済ませる
pub fn load(&mut self, html: &str) {
let document = HtmlParser::parse(html);
self.stylesheet = collect_stylesheet(&document);
self.document = Some(document);
self.run_scripts();
self.relayout();
}
/// DOM と CSSOM から箱の木と表示リストを作り直す
pub fn relayout(&mut self) {
let Some(doc) = &self.document else { return };
self.layout_root = layout_document(doc, &self.stylesheet);
self.display_list = self.layout_root.as_ref().map(paint).unwrap_or_default();
}
fn run_scripts(&mut self) {
let Some(doc) = &self.document else { return };
for script in Node::find_all(doc, "script") {
let source = script.text_content();
let mut rt = JsRuntime::new(Some(doc.clone()));
if let Err(e) = rt.run(&source) {
self.script_errors.push(e.to_string());
}
self.console.extend(rt.console_output);
}
}
/// 文字のキャンバスに描いたもの
pub fn ascii(&self) -> String {
render_ascii(&self.display_list, VIEWPORT_WIDTH)
}
}
手元で動かす(配布物)
- ステップ 14 の 1 ファイル版(step14_browser_api.rs)全部入り。実行すると 3 段の描画結果が出る。テスト 66 本
- 完成版のプロジェクト(tinybrowser.zip)
図やコードに出てくる用語(タップで説明)
学んだ概念を、素朴な例から実務寄りの例まで3つの具体例で確認します。
公式ドキュメントで詳しく ↗