🌏

Angularのレンダリングメカニズムを学ぶときの歩き方

2020/12/11に公開
4

この記事の目的

Angularのドキュメントは、How to useはそれなりにありますが、conceptsが少ないよなあと昔から思っていました。使い方はわかってもアーキテクチャーがよくわからないのです。

どんなコンセプトなのか?、背景にある考え方は何なのか?、どんな仕組みになっているのか?などは、ウェブで幅広く探すしかありませんでした。そこで、私がこれまで参考にしてきたもののうち、特にレンダリングメカニズムに関して設計思想や仕組みがわかるものを集めました。なるべく一次情報のものをリストアップしています。

最近はangular.ioのコンテンツも充実してきましたが、設計思想的なものは相変わらず少なかったりします。Angularを使うことではなくて、Angularそのものに興味を持ったときのガイドになればと思います。他にもこれはというものがあったらぜひコメントなどで教えてください。

なお、APIやクラス名などの実装詳細については、大半のドキュメントがアウトオブデートなのでご注意ください。あくまで設計思想や仕組みを学ぶための参考資料群です。

本記事はAngular Advent Calendar 2020の11日目です。今回は経験者向けに参考資料の紹介という体ですが、パートタイムでお手伝いしているビットバンク株式会社 Advent Calendar 2020の21日目では初心者向けにレンダリングメカニズムを噛み砕いて詳細解説する予定です。ご興味ある方はそちらもぜひご覧ください。

レンダリングメカニズムの概要

Angularのレンダリングメカニズムの主要登場人物と関係性を説明します。雑な絵ですみませんが、どんな要素があるかを把握できるようにしています。Angularの特徴として、1.Incremental DOM方式、2.プッシュ型の更新モデル、3.コンパイル必須が挙げられます。
主に開発時に行う事前工程と、ブラウザで行われる実行工程、そして更新工程の3工程に分かれます。

事前工程と実行工程

  • コンパイラ: ソースコードからビルドファイルを生成する。フレームワークの結合が行われ、htmlもjsコードに変換される。
  • レンダラー: ブラウザがビルドファイルを読み込んだら起動される。仮想ビューを作りDOMを操作する。画面が描画される。
  • Incremental DOM: 実ノードにメタ情報が付与された仮想ノードのツリー。レンダラーによって作られる。

初回ロード時の説明図

更新工程

画面を更新する時の工程です。イベント発生や通信終了をトリガーにして、変更を即時適用するプッシュ型の更新モデルです。

  • Zone: ユーザーの入力や通信の終了をフレームワークに通知する。
  • チェンジディテクション: 更新がトリガーされたら、変更が必要なコンポーネントを検出する。
  • レンダラー: 現状のIncremental DOMを参照して変更を適用する。画面が描画される。
  • Incremental DOM: 仮想DOMとは異なり、基本は元のツリーが書き換えられる。毎回スクラップ&ビルドされる組み込みビューなどの派生系もある。

更新時の説明図

レンダリングメカニズムのコンセプトがわかる

Incremental DOM

  • Angular2以降のベースになっているDOMマネジメント方式。
  • reactなどの他のjsフレームワークと比較して、Angularがどんな特徴なのかがわかりやすい。
  • 主に2つのフォーカスがある。
    1. メモリリソースの節約とガベージ・コレクションの負荷軽減のために、仮想DOMを作るのは最初だけにして、更新は既存のツリーを書き換えるようにする
    2. 仮想DOMでもhtml記法そのままで使えるようにする
  • 実現方法は、物理ノードとメタ情報をミックスしたノードツリーを作る。
    • ツリーのベースを仮想にするか物理(DOM)にするかは実装者の選択になる。
    • Angularは仮想ツリーをベースにしている。DOMノードの参照を保持した仮想ノードでツリーが構築される。
  • コンパイルでhtmlをjsコードに事前生成するアイデアも挙げられている。

Incremental DOMの作者が書いた紹介記事
https://medium.com/google-developers/introducing-incremental-dom-e98f79ce2c5f

DI、ChangeDetection、routerといったAngularの中核メカニズムを作ったVictor Savkin氏のブログ
https://blog.nrwl.io/understanding-angular-ivy-incremental-dom-and-virtual-dom-243be844bf36

Angular2のレンダリングの基本設計

  • angular.js(v1)から仕切り直すにあたって、ベーシックデザインが記載されているドキュメント。Dart版のドキュメントぽいが、基本設計レベルなのでTypeScriptでも十分に参考になると思う。
    • application layerとrender layerに分割する。仮想ビューで抽象化することで、異なる環境/プラットフォームで動けるようにするということが核。
      1. ブラウザのメインプロセスとwebワーカープロセスを跨いで実行できる
      2. サーバーサイドで実行できる
      3. ネイティブアプリを作れる
      4. ブラウザを使わずにUIテストができる
      5. 低性能やバッテリーセーブモードのデバイス向けなどの最適化しやすくなる
    • 大まかな構成やAPI設計
      • application layerとrender layerに加えて、両者をブリッジするRenderer APIの3つの役割構成
      • 初期描画やビューの更新方法
      • DomRendererとNativeScript rendererを分ける
  • こんなことがやりたいよねーということが挙げられていて、DAY1の資料という感がある。現時点ではやりきれていないことが見受けられることも味わい深い。
  • 一般公開されているが基本的にgoogle社内文書らしく、プロジェクト内での位置付けは不明。

https://docs.google.com/document/d/1M9FmT05Q6qpsjgvH1XvCm840yn2eWEg0PMskSQz7k4E/edit

実際に動いている仕組みがわかる

公式ドキュメント

一次情報といえば最初に挙げるべき存在のangular.ioで、設計思想や仕組みに関する章は「Angular Concepts」という章です。内容としては、実際にソースコードやAPIの名称になっている概念についての説明が充実しています。

「Angular Concepts」の章
https://angular.io/guide/architecture

上記で挙げられている内容をざっと一通り解説しているAngularConnectのセッションもおすすめです。
https://www.youtube.com/watch?v=S0o-4yc2n-8

ivy

概要

  • 現時点で最新となるレンダラーの名称。Angular2になってから3世代目となる。
  • 以前よりも更にincremental DOMのAPIに近づいているので、先にincerementalDOMを把握した方が理解しやすい。
  • 大きくランタイムとコンパイラの2つのモジュールで構成される。
    1. ランタイムは、実行時に仮想ビュー層のツリーを参照して変更適用するものである。
    2. コンパイラは、htmlファイルを解析してjsのコードに変えたり、DIを中心にしたフレームワークの結合を事前に行っておくものである。
  • ivyは、1.コンパイル、2.デプロイ、3.ディペンデンシィの3つの単純化を行っている。その結果、軽量化や高速化に加えて、今までやりづらかったエコシステムへの道を開いている。
    • 実行時に必要なものが少なくなったので、WebComponentでのライブラリ配布や他フレームワークとのジョイントなどへの可能性が増している。
    • テンプレートやコンポーネントを作成するAPIが整備されたので、ツール開発やHigher order componentsなどの可能性が増している。

https://docs.google.com/presentation/d/1D0bOl1SST_p9oCgPd71-Z7_bg-FXf_IQeNViXfgqF18/edit#slide=id.g26d86d3325_0_0

実装設計

もっと詳細に知りたいときにガイドになるものを挙げる。

  • ランタイムの設計

https://hackmd.io/@mhevery/BkDUxaW84/%2FK7MLVYSDQKKaMKJXW_KZAw?type=book
/angular/packages/core/src/render3/VIEW_DATA.md
https://github.com/angular/angular/blob/master/packages/core/src/render3/VIEW_DATA.md

  • コンパイラの設計

/angular/packages/compiler/design/architecture.md
https://github.com/angular/angular/blob/master/packages/compiler/design/architecture.md

Change Detection

  • 初回描画後に、画面の更新が必要になる変更を検知するための仕組み。
  • 更新が発生したら、各コンポーネントでビューにバインドしている状態変数が変化したかを調べる。
  • 更新のトリガーとなるのは主に3つ。これを担うのがzoneである。
    1. イベント - click, submit, ...
    2. XHR - サーバーからデータを取得
    3. タイマー - setTimeout(), setInterval()
  • 描画モデルについて、reactはプル型と言っているが、Angularはプッシュ型が基本となる。変更が発生したら即時画面更新するモデルである。

https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html

日本語訳: https://qiita.com/lacolaco/items/523d96ddbfe55c4e6949

Zone

  • プッシュ型の更新トリガーを行うための核になっているもの。
  • フレームワークやテストが非同期処理の終了を即時に把握できるように、終了を伝播するコンテクストを作る。
  • Dartの機能がインスパイアになっている。DartやAngularの実装を元にして、TC-39にプロポーザルが起こされている。
  • そのTC-39のプロポーザル文書が、何を解決しようとするものかや解決策のアイデアを理解しやすい。

https://docs.google.com/presentation/d/1H3E2ToJ8VHgZS8eS6bRv-vg5OksObj5wv6gyzJJwOK0/edit#slide=id.p

Discussion

lacolacolacolaco

Angularのアーキテクチャの概要がわかる記事をありがとうございます!
1点だけ指摘したいのですが、Angularのアーキテクチャ内には "仮想ビューオブジェクト" や "仮想DOM" にあたるデータモデルは登場しないです。
Change Detectionにより各コンポーネントのChangeDetectorが呼び出され、そこで変更があった箇所は Ivyのinstruction (Incremental DOM志向のAPIですね)を経由してRendererを使って即座にDOMを操作します。なので、Angularのビューレンダリングの説明の中で仮想ビュー、仮想DOMというデータモデルは登場しません。
おそらくLView/TViewのことを指しているのだと思いますが、これらはDOM操作のための中間ビューというよりは、Incremental DOMで生成と更新を区別するための参照の保持、変更検知の効率化のためのメモ化、階層的DIを実現するためにコンポーネントツリー構造とDIコンテナの同期など主な役割で、レンダリングに直接は関わってこないです。
たとえばDOM要素を作成するinstruction elementStart はこの関数の中で createElementNodeを呼び出し、その先では render.createElement まで到達しています。AngularがコンポーネントからDOM操作の間に中間層を持っていない様子です。(Angularのプラットフォーム抽象化はRendererの責務ですね)
https://github.com/angular/angular/blob/b08fd0c18c131917577fc4c952c3d9f42b1e54fa/packages/core/src/render3/instructions/element.ts#L72
https://github.com/angular/angular/blob/b08fd0c18c131917577fc4c952c3d9f42b1e54fa/packages/core/src/render3/node_manipulation.ts#L126
もし読み違えていたらすみません。

teatwoteatwo

なるほど、ありがとうございます。なるべく他のフレームワークにも通じるような一般論に寄せてわかりやすくすることと、Angular固有の具体実装の正確性とのバランス舵取りが難しいなあと苦笑いしました。

仮想ビューオブジェクトは、LView/TViewを指しています。解釈の仕方ですが、js空間内で画面の論理情報を保有しているものを仮想ビュー層だと考えています。DOMはあくまでjs空間に対するAPIで、実際に状態を保有しているのはレンダリングエンジン側で、それが物理層だと考えています。その前提で、今の画面状態を参照するものとしてivyのinstructionメソッドがLView/TViewを利用していることが、まさにLView/TViewが仮想ビュー層の役割を担っていることを示していると考えます。

焦点は、1.描画モデルの理解、2.モデルの単純化への許容度だと考えます。一つ目ですが、Change Detectionの段落で挙げているようにAngularはプッシュ型の描画モデルだと理解しています。更新検知から、ivyのinstructionメソッドが仮想ビュー層と物理層の更新を一気に行っているのは、まさにプッシュ型の姿だと思います。2つ目は、現実のあれやこれやのために複雑・高度化したものたちを捨て、原点にあったものを見つけ出したいのです。IncrementalModelをベースにAngularはさらに複雑・高度に発展させていますが、そのベクトルを逆に向けます。本編でピックアップしているドキュメントを見ると、開発者の方がIncrementalDOMを仮想DOMの改良版の位置付けにしていることが確認できます。ただ、私も仮想DOMはあくまでIncrementalDOMの説明のときだけに用いて、Angularの説明ではIncrementalDOMを参照すれば良いと思っています。

問題点は、直線的な処理順番で図解したことだと捉えました。特に仮想ビューオブジェクトを中間状態に置いたことです。これはおっしゃる通りだなと思って、図を書き直しますね。一般論に寄せてわかりやすくすることと、Angular固有の具体実装の正確性とのバランス舵取りが難しいので、少しお時間いただきます。

概念モデルに抽象して丸めようとしているので、ふわふわした内容になることをご容赦ください。わかりづらい箇所がありましたらご質問ください。というか、このように、概念モデルって現実のあれやこれやのために複雑・高度化した実装内容からボトムアップで解き明かしずらいので、開発者から設計思想を打ち出して欲しいんだよなあというのが、まさにこの記事の背景でした苦笑

lacolacolacolaco

仮想ビューオブジェクト、という言葉についての定義づけにやっぱり僕の方で読み取り違いがあったみたいですね。

問題点は、直線的な処理順番で図解したことだと捉えました。

まさにこの点でした!論点を分解してくれてありがとうございます。React等のメカニズムにおける仮想DOMツリーのように、「正」となるメモリ上のオブジェクトがあり、DOMはそれを元に投影される、という主従(?)の関係はないというところですね。 ivyのinstructionメソッドが仮想ビュー層と物理層の更新を一気に行っている のおっしゃるとおりです。

なかなか英語でも少ない視座の内容なのでまとめるのが難しいと思います。貴重なアウトプットありがとうございます 😍

teatwoteatwo

こちらこそフィードバックありがとうございました!ディスカッションでさらに理解が深まりました。

設計思想はソースコードに書かれていないことなので、話題にしづらいですよね。それでもウェブを探せばそれなりにあるもんだということを伝えられれば、この記事は本望です。