77. コンポーネントが画面に生まれる手順 — 実行時の変化
@if が真になった瞬間、ルーティングで画面が切り替わった瞬間、起動時に <app-root> が動き出す瞬間 — コンポーネントが画面に現れるとき、内部では毎回同じ誕生手順が走ります。順に、(1) ホスト要素(<app-user-card>)をDOMに作る、(2) そのノード用のインジェクタを親のインジェクタにつなぐ、(3) コンストラクタを実行する(inject() が動くのはここ)、(4) 親からの input の値を流し込む、(5) ngOnInit などのライフサイクルフックを呼ぶ、(6) テンプレート関数を作成モードで実行し、テンプレート内に子コンポーネントがいればそこで同じ手順を再帰的に繰り返す、という流れです。
この手順の順序が、これまで学んだ約束事の「理由」になっています。コンストラクタの時点では (4) がまだなので、親からの値は届いていません。旧来の @Input プロパティをコンストラクタで読むと undefined だったのはこのためで、「初期化処理は ngOnInit で」という定石はこの順序から来ています(input() シグナルなら、読んだ時点の初期値が返り、後から届く親の値には computed が追従してくれます)。また inject() がコンストラクタなどの注入コンテキストでしか使えないのは、(2) で自分のインジェクタが確定した直後の、Angularが「いまこのコンポーネントを組み立て中」と知っているタイミングだけ、依存の問い合わせ先を特定できるからです。
アプリの起動も、実はこの手順の特殊例です。bootstrapApplication(AppComponent) は「親がいない状態で AppComponent の誕生手順を1回走らせる」ことで、はじめの一歩で学んだ「ほぼ空の index.html から画面が現れるまで」の中身がこれです。そして (6) が再帰であることが性能に直結します。初回表示のコストは配下のツリー全体の誕生手順の合計なので、@defer で境界を切ると速くなるのは「ツリーの一部の誕生を後回しにする」からだ、と内部の言葉で説明できるようになります。
サンプルコード(フレームワーク環境が必要なため表示のみ)
// @if (show()) { <app-user-card [name]="userName()" /> } で
// show() が true になった瞬間に走る誕生手順:
//
// (1) ホスト要素の作成
// document.createElement('app-user-card') 相当 → DOMへ挿入
//
// (2) インジェクタの接続
// このノード用のDI窓口を作り、親インジェクタへ連結
// (DIの応用で学んだ「提供範囲」のツリーがここで伸びる)
//
// (3) コンストラクタ実行
// inject(UserService) はここで (2) の窓口に問い合わせる
// ※ input の値はまだ届いていない!
//
// (4) input の設定
// 親の userName() の現在値が name に流し込まれる
//
// (5) ライフサイクルフック
// ngOnInit など(だから初期化はここが安全)
//
// (6) テンプレート関数を作成モードで実行
// 子コンポーネントがいれば (1) から再帰
学んだ概念を、素朴な例から実務寄りの例まで5つの具体例で確認します。
公式ドキュメントで詳しく ↗