Chapter 02無料公開

ビュー

lacolaco
lacolaco
2021.12.18に更新

ビューはひとかたまりに処理される表示要素のグループである

Angularにおける ビュー(view)は、UI描画の過程においてひとまとまりに生成・更新・破棄される表示要素の最小グループであると定義される。ビューはその生成の仕方から三種類に分けられる。

第一のビューはルートビュー(root view)である。これはAngularアプリケーションの実行単位ごとに一つだけ存在し、他のすべてのビューを内包するビューである。
ルートビューはAngularアプリケーションの起動時に生成され、アプリケーションの終了時に破棄されるが、開発者がルートビューの存在を意識することはまれである。

第二のビューはコンポーネントビュー(component view)である。その名の通り、これはAngularコンポーネントに紐付いたビューである。コンポーネントのインスタンスは、そのコンポーネントのテンプレートが定義するビューに紐付けられる。

第三のビューは埋め込みビュー(embedded view)である。埋め込みビューはコンポーネントのテンプレートの一部として定義される。埋め込みビューを作るのは、構造ディレクティブ(structual directive)または<ng-template>タグによって宣言されるテンプレートである。このビューは、コンポーネントやディレクティブの処理によって別のビューの内部に埋め込まれることで描画される。

ビューは階層構造を作る

Angularアプリケーションを起動(bootstrap)するためには、ルートモジュールの bootstrap フィールドで宣言されるルートコンポーネント(root component)が必要である。ルートコンポーネントはアプリケーションを構成するコンポーネントツリー(component tree)の頂点になる特別なコンポーネントだが、それと同時に、他のコンポーネントと同じようにテンプレートによって定義されるコンポーネントビューを持つ。

前述のとおり、あらゆるビューはルートビューに内包される。ルートコンポーネントのコンポーネントビューは、ルートビューが内包する最初のビューとなる。こうしてビューにもっとも小さい階層構造が生まれる。さらに、ルートコンポーネントのテンプレートで他のコンポーネントを呼び出すと、コンポーネントビュー同士の親子関係により階層構造が拡大する。

このようにビューはルートビューを頂点とした階層構造を作るが、はじめに定義したとおりビューとはひとまとまりに生成・更新・破棄される表示要素の最小グループである。生成・更新・破棄をライフサイクルと言い換えれば、同じライフサイクルで管理される表示要素の最小グループと呼ぶこともできる。どちらにせよビューは表示要素のグループであるが、下位のビューは上位のビューにとって表示要素のひとつであるという点が重要である。

下位のビューを処理せず上位のビューだけを処理することはできない

これはコンポーネントツリーから考えればわかりやすい。末端のコンポーネントビューはそのコンポーネントテンプレートに直接記述された表示要素だけを管理しており、そのコンポーネントビューは単体でライフサイクルが完結している。だがひとつ上の階層にあるコンポーネントビューは単体では完結しておらず、下位のビューのライフサイクルを内包しなければならない。このことは実際にAngularのライフサイクルメソッドの呼び出し順から確認することもできる。2つのコンポーネントが親子関係にあるとき、ngOnInit()ngAfterViewInit()は次のような順番で呼び出される。

  1. ngOnInit()
  2. ngOnInit()
  3. ngAfterViewInit()
  4. ngAfterViewInit()

あるビューの描画処理は、それを内包する上位のビューの描画処理の一部として開始される。そして、上位のビューの描画処理の完了はすべての下位のビューの描画処理の完了を待つことになる。このことから、下位のビューを処理せず上位のビューだけを処理することはできないという原則が導かれる。また、ルートビューが最上位のビューであるということは、すなわちアプリケーションの描画処理がルートビューの描画処理と同義だということである。したがって、アプリケーションの描画処理はルートビューから開始し、ルートビューの描画完了をもって終わる。

変更検知はビューの階層構造にしたがって実行される

変更検知(change detection)は、AngularフレームワークがアプリケーションのUIの状態をデータの状態と同期させるメカニズムであると定義される。つまり、ビューが管理する状態とコンポーネントインスタンスの状態を比較し、差異があればコンポーネントインスタンスの状態を基にビューを再描画することを指す。

変更検知がビューの描画処理であるということは、変更検知もまたルートビューから階層構造にしたがって連鎖的に実行されることを意味する。つまり、上位のビューで変更検知が行われた結果が下位のビューの描画へ影響するが、逆に下位のビューの変更検知が上位のビューへ影響することはない。ただし、コンポーネントの実装の誤りによりそのような振る舞いが発生することはありえるため、変更検知により何らかの副作用が発生したときには、AngularはExpressionChangedAfterItHasBeenCheckedErrorをスローして開発者に警告する。

また、ChangeDetectorRefは、開発者が変更検知を明示的に実行するために使用できるAPIである。ひとつのChangeDetectorRefインスタンスは特定のビューの変更検知を制御する。つまりビューのインスタンスとChangeDetectorRefのインスタンスは1:1に対応している。このクラスが提供するdetectChanges()メソッドは、対象のビューだけでなく子孫となる下位のビューでも変更検知を実行する。これも上位のビューだけを処理することができないためである。

OnPushストラテジーは変更検知の実行に条件を加える

ところで、「下位のビューを処理せず上位のビューだけを処理することはできない」という原則には例外がある。それはコンポーネントの変更検知ストラテジーOnPushに設定されている場合である。デフォルトの変更検知ストラテジーでは、上位のビューで変更検知が実行されるとその下位のビューも連鎖的に変更検知が実行される。つまり、ルートビューから変更検知が開始すればすべてのビューが変更検知されるということになる。これは整合性を守るための振る舞いだが、アプリケーションが大きくなると実行時パフォーマンスの低下を招く。
OnPushストラテジーは特定のビューの変更検知を実行される条件を限定し、整合性の確保を開発者に移譲する代わりにパフォーマンスを最適化可能にする。その条件の詳細はここでは割愛するが、下位のビューがOnPushストラテジーであればその変更検知をスキップして上位のビューの変更検知を終えることができる。