Angularのライフサイクルについて

最近Angularについて勉強しているのでそこでの学びをアウトプットしようと思います. Angularのコンポーネントには,Angularがコンポーネントクラスをインスタンス化して,コンポーネントビューとその子ビューをレンダリングする時に開始するライフサイクルがあります.そして,別のページに遷移するなどを行うとライフサイクルが終了します. 今回はそのライフサイクルに合わせてフックされるライフサイクルフックメソッドを使ってAngulrarのライフサイクルについて説明します. Angularのライフサイクルフックメソッドは以下のような順番で呼び出されます.

f:id:harada-777:20201129130424p:plain:w200

主なメソッドについて説明します. - ngOnChanges - 入力プロパティを設定またはリセット,変更する度に呼び出される. - ngOnInit - Angularが入力プロパティを設定したあと,最初にコンポーネントを初期化するタイミングで呼び出される - ngDoCheck - コンポーネントの状態が変わる度に実行される.(Change DetectionというAngularの状態管理の仕組みが実行される度にこのメソッドがフックされる) - ngOnDestroy - コンポーネントが破棄されるタイミングで呼び出される.つまりDom上から削除されるとき.

コンポーネントの例

実際に呼び出されていることを確認するために,以下のような親と子コンポーネントを作成しました.以下に作成したアプリケーションのリンクを貼っておきます. https://github.com/Yoshiaki-Harada/angular-lifecycle

コンポーネント
import { Component, DoCheck, OnChanges, OnDestroy, OnInit, SimpleChanges } from '@angular/core';

@Component({
  selector: 'app-parent',
  templateUrl: './parent.component.html',
  styleUrls: ['./parent.component.css']
})
export class ParentComponent implements OnInit, OnChanges, OnDestroy, DoCheck {
  message: string
  isChiled = true

  constructor() {
    console.log('[Parent] constructor')
  }
  ngOnInit(): void {
    console.log('[Parent] ngOnInit')
  }

  ngDoCheck(): void {
    console.log('[Parent] ngDoCheck')
  }
  ngOnChanges(changes: SimpleChanges): void {
    console.log('[Parent] ngOnChanges')
  }

  ngOnDestroy(): void {
    console.log('[Parent] ngOnDestroy')
  }

  toggleChildView() {
    this.isChiled = !this.isChiled
  }
}
<h2>親コンポーネント</h2>
<label for="message">メッセージ:</label>
<input type="text" name="message" id="message" [(ngModel)]="message">
<br>
<button (click)="toggleChildView()" type="button">子コンポーネントの表示を切り替える</button>
<div class="child" *ngIf="isChiled">
    <app-child [message]="message"></app-child>
</div>
コンポーネント
@Component({
  selector: 'app-child',
  templateUrl: './child.component.html',
  styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges, DoCheck, OnDestroy {
  @Input() message: string
  constructor() {
    console.log('[Child] constructor')
  }
  ngOnInit(): void {
    console.log('[Child] ngOnInit')
  }

  ngDoCheck(): void {
    console.log('[Child] ngDoCheck')
  }

  ngOnChanges(changes: SimpleChanges): void {
    console.log('[Child] ngOnChanges')
  }

  ngOnDestroy(): void {
    console.log('[Child] ngOnDestroy')
  }
}
<h3>子コンポーネント</h3>
<p>親から受け取ったメッセージは: {{message}}</p>

実行

このプログラムを実行しアクセスしてみると以下のようなログが出力されます. f:id:harada-777:20201129130428p:plain

これを見て頂くと前述の図の通りの動作していることが確認できます. 注意する点としてはコンストラクタが一番初めに呼び出されている点です.Angularは様々な初期化処理をコンストラクタではなく,ngOnInitでやることが推奨されています.コンストラクタが呼び出された時点では,入力プロパティに値がバインドされていない状況であり,コンポーネントが完成されていないからです.これは子コンポーネントのngOnInitがconstructor→ngOnChanges→ngOnInitと呼び出されていることからもわかります. また親コンポーネントのngOnChangesが呼び出されていないことがわかります.これは親コンポーネントには入力プロパティがないからです.Angularは入力プロパティがなく,受け取るものがないコンポーネントのngOnChangesをスキップします.

コンポーネントの変更

次に値を入力してみます.するとログには以下のように出力されます.

f:id:harada-777:20201129130410p:plain:w400

親,子コンポーネントともにコンポーネントは既に作成されているのでngOnInitは呼び出されていません. コンポーネントが変化したことを検知して,親のngDoCheckが呼び出されていることがわかります.また入力プロパティが変更されたため,子のngOnChangesが呼び出されています. これらのメソッドは以下のように一文字ずつ呼び出されます.

f:id:harada-777:20201129130414p:plain:w300

値が入力する度に特別な何かを行いたい時は,ngDoCheckに実装をすればOKです.しかしこれらのフックメソッドは何度も呼び出されるため,気を付けて実装しないとアプリケーションが重たくなってしまいます.

コンポーネントの破棄

最後にコンポーネントの破棄時に実行されるngOnDestroyについて確認します.(今回はngIfというDomの追加や削除が可能な構造ディレクティブを使って子コンポーネントを破棄しています.)

コンポーネントの表示を切り替えるを押すとコンポーネントが削除されます.

ログの出力から子のngOnDestroyが呼び出されていることがわかります. このngOnDestroyの用途の例はコンポーネントが破棄されるタイミングでsubscribeしているObserableをunsubscribeしたいときです.

コンポーネントに1秒ごとに値を発生させるintervalをsubscribeしてみます.

  ngOnInit(): void {
    console.log('[Child] ngOnInit')
    this.subscription = interval(1000).subscribe(value => {
      console.log('[Chile] number:' + value)
      this.number = value
    })
  }

ngOnDestroyでこれをunsubscriibeしないで,実行してみます.

f:id:harada-777:20201129130434p:plain:w500

number: 6と出力されたあたりで表示の切り替えを2回ほど押すと,number: 0が出力されてしまいました.そしてその後number: 7が出力されています.これはコンポーネントが破棄されても,subscribe状態が続いておりメモリリークが起きてしまっていることを表しています.またnumber: 0が出力し始めているのはまたコンポーネントが生成されたタイミングで新たにsubscribeしたからです.

これを防ぐには以下のようにngOnDestroyでunsubscribe呼び出せばOKです.

  ngOnDestroy(): void {
    console.log('[Child] ngOnDestroy')
    this.subscription.unsubscribe()
  }

まとめ

Angularのライフサイクルについて解説しました.あまり使わないメソッドもありますが,初期化と破棄のタイミングではやりたいことがあることが多いので気を付けて実装しないといけないなと感じました.