Dev Study
← Angular コース

79. 破棄と再利用 — コンポーネントが消えるときに起きること

@if が偽になる、ルート遷移で画面が入れ替わる、@for の配列から要素が消える — コンポーネントが画面から消えるときは、誕生手順の逆再生が走ります。子から順に ngOnDestroy と DestroyRef に登録されたコールバックを呼び、Angularが付けたイベントリスナーを外し、LView を親のツリーから切り離し、最後にDOMノードを取り除きます。シグナルの応用で学んだ takeUntilDestroyed が「勝手に解除してくれる」のは、この逆再生の手順に購読解除を登録していたからです。

破棄が性能問題になるのは、たいてい「破棄→再誕生」がセットで大量に起きるときです。@for の track はまさに「破棄せずに再利用してよいか」の判定材料で、track の値が同じ要素は既存の LView とDOMを使い回して更新モードだけを走らせ、値が変わった要素は破棄→誕生をやり直します。track $index のまま並べ替えを行うと大量の破棄+再誕生が発生する、という @for のレッスンの注意は、この内部動作の話でした。同じ理由で、ルーターは「同じルートでパラメータだけ変わった」場合にコンポーネントを破棄せず再利用します — ルートパラメータを購読で受け取る必要があったのは、ngOnInit が2回目は呼ばれないからです。

最後に、破棄漏れの正体も内部の言葉で説明できます。逆再生の手順が片付けてくれるのは、Angularが台帳に記録したものだけです。自分で呼んだ setInterval、window に直接付けたリスナー、外部ライブラリが作ったオブジェクトは台帳に載っていないので、コンポーネントが消えても残り続けます(メモリリーク)。DestroyRef に登録する習慣は「Angularの破棄手順に、自前の後始末を相乗りさせる」ことに他なりません。ちなみにSSRのハイドレーションは「破棄→再構築をスキップしてDOMを引き継ぐ」仕組みでした — 誕生・更新・破棄という本セクションの地図の上に、コースで学んだ機能が全部載っていることが見えてきたはずです。

サンプルコード(フレームワーク環境が必要なため表示のみ)

// @for (item of items(); track item.id) { <app-row [item]="item" /> }
//
// items が [A, B, C] → [C, A] に変わったときの内部判定:
//
//   C: track値が既存と一致 → 再利用(台帳とDOMを使い回し、
//      位置の移動と更新モードだけ。誕生手順は走らない)
//   A: track値が既存と一致 → 再利用(同上)
//   B: 対応する track値が消えた → 破棄の逆再生を実行
//      (1) 子から順に ngOnDestroy / DestroyRef のコールバック
//      (2) Angularが付けたイベントリスナーを解除
//      (3) LView をツリーから切り離す
//      (4) DOMノードを除去
//
// もし track $index だったら:
//   並べ替えのたびに「中身が別物」と判定され、
//   再利用できるはずの行でも破棄+再誕生が多発する
サンプルコードで身につける(5本) →

学んだ概念を、素朴な例から実務寄りの例まで5つの具体例で確認します。

公式ドキュメントで詳しく ↗