はじめに
実務で3年ほどAngularを使用していましたが、いわゆるSPA(シングルページアプリケーション)がフレームワークとしてどのようにDOMの操作をしているのか、つまり画面の再描画の仕組みについて理解していませんでした。
色々と調べていると仮想DOMやNodeといった聞き慣れない単語が出てきて、更にそこから調べて進めてみるとAngularのレンダリングメカニズムについて非常にわかりやすく解説されている記事に出会いました。
著:Toshiya Tanaka 氏
本記事では、上記で書かれている内容を備忘録程度に簡単にまとめてみようと思います。
詳しく知りたい方は元の記事をご参照ください。
概要
- Angular登場前後のDOMマネジメントの仕組み
- AngularのDOMマネジメント
- 描画や更新のメカニズム
Angular登場前後のDOMマネジメントの仕組み
- 2010年代前半をピークに、リッチなフロントエンドアプリ(インタラクティブなゲームや規模の大きな業務アプリ)を作る際はDOMをほぼ直接操作することが当たり前だった
- HTML要素の多くの属性やイベントを手続き的に管理・操作
- jQueryが登場するも、DOM操作のショートハンディングを提供するのみでDOMの手続き的操作のパラダイムを買えるものではなかった
- そんな中、DOM操作にデータバインディングを被せてDOMマネジメントを簡易化するEmberやAngular(v1)が登場
- 更にその後、仮想DOMを提供するReactも登場
- Angular(v1)はHTML拡張というアプローチであったが、Reactはall jsのアプローチで、物理的なDOM層を完全に隠蔽した「仮想ビュー層」という新しいレイヤーを追加
- 仮想ビューはjs空間内で、DOMとは別に画面状態の論理情報を一通り保有する層(DOMはあくまでjs空間に対するAPIであり、状態が格納されているのはレンダリングエンジン側)
- これにより、DOM操作を開発者が直接触らずに済むようになる
※レンダリングエンジンについて知りたい方はこちら
https://zenn.dev/syommy_program/articles/d868adf9ba3225
つまり、今まで直接操作する必要があったDOMを意識しなくても済むようになったんですね。
AngularのDOMマネジメント
前項のDOMマネジメントの仕組みでは、AngularはHTML拡張で、仮想ビュー層の仕組みを持つのはReactとありました。
...ではAngularは仮想ビューを持たない?
実はAngularも仮想ビュー層を持っています。
Angularはv1とv2以降で大きく仕組みが変わっており、
- v1
- HTML拡張方式で、アドオンによりjs側で追加・拡張していく方式(ディレクティブと呼ぶ)
- 拡張されたHTMLをブラウザが読み込み、jsが後から動く
- v2以降
- Reactと同様、js空間にある仮想ビュー層で画面状態の論理情報を管理
- AngularはReactと異なりHTMLファイルが存在するが、各々のHTMLファイルはビルド後にjsコードとなる
- HTMLファイルとしてブラウザに読み込まれるわけではなく、実行時にjsファイルから画面生成指示が行われており、その点ではReactやVue.jsと同様
ではAngularも仮想DOM方式なのか?
→ v2以降は仮想DOMを改良した「IncrementalDOM方式」を採用。
これとAngularの特徴であるDIシステムを組み合わせることでビュー層が形成されています。
仮想DOMに対して、IncrementalDOMが持つ強みは下記の2点。
- メモリリソースの節約とガベージ・コレクションの負荷軽減
- 仮想DOMを作るのは最初だけで、更新は既存のツリーを書き換えるような仕組み
- そのためにリアルDOMと仮想DOMを分けず、リアルノードにメタ情報を追加したバーチャルノードを作成してツリーを作成している
- 更新時は、このツリーをトラバースして変更するノードを見つける。その仮想ノードはリアルノードの参照を持っているため、リアルDOMを更新することができる
- HTML記法をそのまま使える
※Angularが採用しているのは純粋なIncrementalDOMとは大きく異なるようです。
描画や更新のメカニズム
Angularのレンダリングメカニズムには3つの特徴があります。
- IncrementalDOM方式
- 前項で紹介
- プッシュ型の更新モデル
- 描画後の更新モデル。Angularでは何か変更があった際に即時更新するプッシュ型のモデルを採用
- コンパイル(トランスパイル)必須
- TypeScriptで書くことが必須であることに加え、フレームワークのための解析加工処理も行われる
上記の特徴を踏まえ、ビルド→描画→画面更新の流れは以下のようになります。
※元記事のようなソースベースでの解説はこちらでは割愛しますが、非常に参考になるので元記事もぜひご一読ください。
- ビルド
- Angularは、JIT(Just-in-time)ではなく基本的にはAoT(Ahead-of-time)で、事前に静的解析してビルドファイルを作成しておく
- ビルドでは、ただTypeScriptをJavaScriptにトランスパイルしたり最適化しているだけでなく、フレームワークとして実行に必要な加工処理を行う
- HTMLファイル→JavaScriptコードの変換、@'Componentや@'Injectableのようなデコレータで記述されているメタデータや、NgModuleとして記述されているコンパイルコンテクストなどの解決など
- 以下、ビルド方法
- HTMLをjsの生成関数に変換する
- CSSやアニメーションもjsコードに整理される
- コンパイルコンテクストに基づきDIやModuleの解決や結合が行われる
- 描画
- ブラウザがビルドファイルを読み込むと、生成関数が動きソースコードで記述していたテンプレートが画面に描画される
- この過程では先述の生成関数が動くだけでなく、DIが核になりさまざまなサービスの注入やその機能が実施される
- 画面更新
- 更新する方法
- 主だった要素作成やイベントリスナーの付与などの重い処理は初回生成時に一度だけ行うようにして、以後は更新用のコードブロックだけが繰り返し実行される
- 更新するトリガー
- 変更の発生時
- 変更検知は「zone.js」というライブラリを使用することで、非同期関数の解決を検知している
- 上記により、UIイベント(click, submit, ...)、サーバーからの通信、タイマー(setTimeout(), setInterval())といった非同期スタックの終了検知を実現
- 更新する箇所の特定
- 上記で記載したzone.jsの通知をトリガーとし、画面に描画されているコンポーネントのツリーのどこに変化が起こったどうかを調べるアルゴリズムを実施
- この変更検知のアルゴリズムおよび実行モジュールをチェンジ・ディテクション(ChangeDetection)と呼ぶ
- これにより変更が起こったと判定されたコンポーネントを洗い出し、そのコンポーネントのテンプレートの更新用関数だけを実行する
- 更新する方法
最後に
HTMLのレンダリングやDOMの仕組みがわかっていなかった私にとって、最初に紹介した記事が非常に参考になりました。
DOMや仮想DOMについてもっと深く知りたい方は下記の記事もおすすめです。
- 【JavaScriptの基本】DOMの仕組みと構造
- 【React】仮想DOMって何!?コンポーネントのレンダリングと描画について理解しよう!
- 仮想DOMは純粋なオーバーヘッド(Virtual DOM is pure overhead)
元の記事を読み返していて熟練者向けの記事があるみたいだったので、そちらもぜひ。(元記事の「おわりに」より)
最後まで読んでいただきありがとうございました。