LoginSignup
7
5

More than 5 years have passed since last update.

angular.io Guide: LifeCycle Hooks

Last updated at Posted at 2017-07-09

これは、Angular の公式ドキュメントのライフサイクルフックの章を意訳したものです。所々抜け落ち、翻訳モレがありますがあしからずご了承を。
バージョン 4.2.6 のドキュメントをベースにしています。

Lifecycle Hooks

コンポーネントはAngularによって管理されたライフサイクルフックを持っています。

Lifecycle Hooks

  • 緑色のブロック ... Component, Directive どちらでも実行可能
  • 青色のブロック ... Component だけでのみ実行可能なライフサイクルメソッド
  • 薄い色のブロック ... 何度でも実行される
  • 濃い色のブロック  ... 一度だけ実行される

コンポーネントを作成し、レンダリングし、また子コンポーネントを作成し、レンダリングし、さらにデータバインドプロパティが変更されたときに、それをチェックし、DOMからそれを削除する前に、それを破壊します。

ディレクティブも同様にライフサイクルフックを持っています。コンポーネントよりも少ない

コンポーネント ライフサイクルフックの概要

ディレクティブとコンポーネントのインスタンスは、Angularが作成、更新、破棄する際に発火するライフサイクルを持っています。

ライフサイクルフックはAngularのcoreライブラリのインターフェースとして実装されており、開発者は、そのライフサイクルにおけるタイミングで処理を発火させることができます。

ディレクティブについては、コンポーネントのみで意味をなすライフサイクルフックについては実装されていません。

ライフサイクルシーケンス

コンストラクタを呼び出してコンポーネント/ディレクティブを作成すると、Angularは、特定のタイミングで、次の順序でライフサイクルフックメソッドを呼び出します。

ngOnChanges()

Angularがデータバインドされた入力プロパティをセットするときに実行します。
1つまたは複数のデータ・バインドされた入力プロパティが変更するたびに実行されます。
ngOnInit() の前に呼び出されます。

ngOnInit()

ディレクティブ/コンポーネントの入力プロパティを設定し、データバインドされたプロパティを表示できるようになると実行されます。
ngOnChanges() の初回実行後に一度だけ呼ばれます。

ngDoCheck()

ngOnChanges()ngOnInit() の実行直後に1度、
また変更検知(Change Detection)が走る度に呼び出されます。

ngAfterContentInit()

AngularがComponentにコンテンツを投入した後に呼ばれます。
ngAfterContentInit() の後と、すべての ngDoCheck() の後に呼び出されます。
※Componentでのみ機能します

ngAfterContentChecked()

Angularがコンポーネントにコンテンツを投入するチェックが実行された後に呼ばれます。
ngAfterContentInit() の後と、すべての ngDoCheck() の後に呼び出されます。
※Componentでのみ機能します

ngAfterViewInit()

コンポーネントのビューと子ビューが初期化された後に呼び出されます。
最初の ngAfterContentChecked() の後に、一度だけ呼び出されます。
※Componentでのみ機能するフックです。

ngAfterViewChecked()

Angularによってコンポーネントのビューと子ビューがチェックされた後に呼び出されます。
ngAfterViewInit() の後と、すべての ngAfterContentChecked() の実行後に呼び出されます。
※Componentでのみ機能するフックです。

ngOnDestroy()

ディレクティブ/コンポーネントを破棄する前のクリーンアップとして用います。
Subscriptionを解除したり、メモリリークを防ぐためにイベントハンドラを切り離すために用いると良いでしょう。
ディレクティブ/コンポーネントが破棄される直前に呼び出されます。

インタフェースはオプションです(技術的な話)

技術的な観点で純粋にお伝えすると、インターフェイスはJavaScriptとTypeScriptの開発者のためのオプションです。

Angularではディレクティブとコンポーネントのclassをチェックした上でフックメソッドが定義されていれば呼び出します。

だけども、エディタツールからメリットを得るため、TypeScriptを用いて、ディレクティブクラスでインターフェイスを追加することをお勧めします。

他の Angular ライフサイクルフック

他の Angular サブシステムは、これらのコンポーネントフックとは別に、ライフサイクルフックを持つことがあります。
サードパーティのライブラリは、開発者が自身のライブラリの使い方をより詳細に制御できるように、独自のフックを実装することもできます。

ライフサイクルのサンプル

各エクササイズの簡単な説明は次のとおりです。

Peak-a-boo: all hooks

peek-a-booは、Angularが予想される順序でフックを呼び出す方法を示すためのデモとして用意しています。
PeekABooComponentはすべてのライフサイクルメソッドを記述しているけど、実際にこのような実装になることはめったにありません。

一連のログメッセージは、OnChangesOnInitDoCheck(3x)、AfterContentInitAfterContentChecked(3x)、AfterViewInitAfterViewChecked(3x)、およびOnDestroyの順番に従います。

コンストラクタは、Angularのフックそのものではありません。
入力プロパティ(この場合は name プロパティ)に、コンポーエント構築時に割り当てられた値が入っていないことを、ConsoleのLogで確認しましょう。

ユーザーがUpdate Heroボタンをクリックした場合、ログには別のOnChangesと2つの3個セット(DoCheck, AfterContentChecked, AfterViewChecked)が表示されます。
明らかに、これら3つセットになっているフックは頻繁に発火していますよね?
これらのフックのロジックを可能な限り把握しておきましょう!

Spy: OnInit と OnDestroy

Spyサンプルで、エレメントの初期化と破棄時の挙動を確かめましょう。

これは、ディレクティブによる完璧なスパイ活動です!😎
ヒーローたちは、Spy ディレクティブによって監視(watch)されていることを決して気付かないでしょう!w

冗談はさておき、このサンプルでは、2つの重要な点に注意しましょう。

  1. Angular は、ディレクティブとコンポーネントのフックメソッドを呼び出します。
  2. spy directive は、直接変更できないDOMオブジェクトのインサイト(見通し)を提供します。 明らかにネイティブの <div> の実装を弄ることはできません。サードパーティーコンポーネントも変更できません。しかし、ディレクティブからは両方の動きを見ることができます。

spy directiveは、ngOnInit()ngOnDestroy() のフックでシンプルに構成されています。注入された LoggerService を介して親にメッセージを記録させる、タチの悪い directive です。

// Spy on any element to which it is applied.
// Usage: <div mySpy>...</div>
@Directive({selector: '[mySpy]'})
export class SpyDirective implements OnInit, OnDestroy {

  constructor(private logger: LoggerService) { }

  ngOnInit()    { this.logIt(`onInit`); }

  ngOnDestroy() { this.logIt(`onDestroy`); }

  private logIt(msg: string) {
    this.logger.log(`Spy #${nextId++} ${msg}`);
  }
}

スパイをネイティブコンポーネントまたはコンポーネントに適用すると、そのエレメントの生成・破棄時に併せるように、スパイを初期化・破棄します。
ここではそれが繰り返しHeroの <div> につきまといます。

<div *ngFor="let hero of heroes" mySpy class="heroes">
  {{hero}}
</div>

各スパイの誕生と死は、付属のhero <div> の誕生と死を、下記に示すようにフックログのエントリでマークします。

ヒーローを追加すると、新しいヒーロー <div> になります。
スパイのngOnInit() はそのイベントを記録します。

Reset ボタンはヒーローリストをクリアします。
Angularはすべてのヒーロー <div> 要素をDOMから削除し、
Spy Directive を同時に破棄します。Spy Directiveの ngOnDestroy() メソッドは最期の瞬間を報告します。

OnInit()

2つの大きな理由から ngOnInit() を使いましょう。

  1. コンポーネントが構築された直後に、複雑な初期化を素早く実行するため。
  2. AngularがInputプロパティを設定した後にコンポーネントをセットアップするため。

経験豊富な開発者は、コンポーネントを簡潔で安全に構築する必要があることに賛同してくれるでしょう。

Misko Hevery氏(Angular チームリーダー)は、複雑なコンストラクターロジックを避けるべき理由 について説明しています。

コンポーネントコンストラクタでデータをfetchしないでください。
新しいコンポーネントが、テスト中に作成されたときまたは表示する前に、リモートサーバーに接続しようとすることを心配するべきではありません。
コンストラクターは初期のローカル変数を単純な値に設定するだけで済みません。

ngOnInit() は、コンポーネントが初期データを取得するのに適しています。
ヒーローズチュートリアルHTTP クライアントではどのように表示するべきか説明しています。

ディレクティブのデータバインドされた入力プロパティは、構築後まで設定されません。
この仕様は、それらのプロパティに基づいてディレクティブを初期化する必要がある場合には問題になりますが、ngOnInit()が実行されたときに設定されます。

ngOnChanges() メソッドは、これらのプロパティにアクセスする最初の機会です。 Angularは ngOnInit() の前に ngOnChanges() を呼び出し、その後何度も呼び出します。ngOnInit() は一度だけ呼び出します。

Angular ではコンポーネントが生成された後、すぐに ngOnInit() メソッドを呼び出すことができます。これは、重い初期化ロジックが属する場所です。

OnDestroy()

ngOnDestroy() でクリーンアップロジックを実行します。
これはAngularがディレクティブを破棄する前に、実行する必要があるロジックがあれば、その処理を書く場所となります。

これは、アプリケーションの別の部分にコンポーネントが消えていくのを通知するための時間です。

これは自動的にガベージコレクションされないリソースを解放する場所です。
ObservablesとDOMイベントの登録を解除したり、setInterval のタイマーを停止したり。
このディレクティブがグローバルまたはアプリケーションサービスに登録したすべてのコールバックを登録解除します。
あなたはこの処理を怠ると、メモリリークの危険性があります。

OnChanges()

Angularは、コンポーネント(またはディレクティブ)の入力プロパティの変更を検出するたびに、そのngOnChanges() メソッドを呼び出します。
この例では、OnChanges フックを監視します。

on-changes.component.ts (抜粋)

ngOnChanges(changes: SimpleChanges) {
  for (let propName in changes) {
    let chng = changes[propName];
    let cur  = JSON.stringify(chng.currentValue);
    let prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

ここに抜粋した ngOnChanges() メソッドは、変更された各プロパティ名を、現在および前のプロパティ値を保持する SimpleChange オブジェクトにマップするオブジェクトを取ります。
このフックは、変更されたプロパティを反復処理してログに記録します。

サンプルコンポーネントである OnChangesComponent には、heropower という2つの入力プロパティがあります。

src/app/on-changes.component.ts

@Input() hero: Hero;
@Input() power: string;

親コンポーネントにあたる OnChangesParentComponent には、次のように記述してバインドします。

src/app/on-changes-parent.component.html

<on-changes [hero]="hero" [power]="power"></on-changes>

ここでは、ユーザーが変更を加えたときの実際のサンプルを示します。

OnChanges

ログエントリは、power プロパティの変更の文字列値として表示されます。しかし ngOnChangeshero.name への変更をキャッチしません。これは最初は驚くべきことでしょう。

Angularは、入力プロパティの値が変更されたときにのみフックを呼び出します。
ヒーロープロパティの値は、ヒーローオブジェクトへの参照です。 Angularは主人公の名前が変わったことに気づかない。
ヒーローオブジェクトリファレンスは変更されていませんでしたので、Angularの観点からは報告する変更はありません!

DoCheck()

DoCheck フックを使用して、Angularが自動的にキャッチしない変更を検出して対応します。

Angularが見落とした変更を検出するには、このメソッドを使いましょう。

DoCheck サンプルは、次の ngDoCheck() フックを使用して OnChanges サンプルを拡張します。

DoCheckComponent (ngDoCheck)

ngDoCheck() {

  if (this.hero.name !== this.oldHeroName) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
    this.oldHeroName = this.hero.name;
  }

  if (this.power !== this.oldPower) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
    this.oldPower = this.power;
  }

  if (this.changeDetected) {
      this.noChangeCount = 0;
  } else {
      // log that hook was called when there was no relevant change.
      let count = this.noChangeCount += 1;
      let noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
      if (count === 1) {
        // add new "no change" message
        this.changeLog.push(noChangeMsg);
      } else {
        // update last "no change" message
        this.changeLog[this.changeLog.length - 1] = noChangeMsg;
      }
  }

  this.changeDetected = false;
}

このコードは、特定の値を検査し、現在の状態を以前の値と比較してキャプチャして比較します。
それは、ヒーローやパワーに実質的な変更がないときに特別なメッセージをログに書き込むので、DoCheckの呼び出し頻度を見ることができます。結果と照らし合わせて見ましょう:

ヒーローの名前が変更されたときに ngDoCheck() フックで検出することもできますが、恐ろしいコストがかかります。
このフックは、変更が発生した場所にかかわらず、すべての変更検知(Change Detection)サイクルの後に非常に頻繁に呼び出されます。この例では、ユーザーが何かをする前に20回以上呼び出されています。

これらの初期チェックのほとんどは、Angularが最初に関連しないデータをページ上の他の場所でレンダリングすることによってトリガーされています。別の <input> にマウスを移動すると、呼び出しが開始されます。

関連するデータへの実際の変更を明らかにするコールは比較的少ないですよね。明らかに、私たちの実装は非常に軽いものでなければいけません。ユーザーエクスペリエンスも低下します。

AfterView

AfterViewサンプルでは、​​コンポーネントの子ビューを作成した後、Angularが呼び出す AfterViewInit() および AfterViewChecked() フックを調べることができます。

ChildComponent

@Component({
  selector: 'my-child-view',
  template: '<input [(ngModel)]="hero">'
})
export class ChildViewComponent {
  hero = 'Magneta';
}

AfterViewComponent は、この子ビューをテンプレート内に表示します。

AfterViewComponent (template)

template: `
  <div>-- child view begins --</div>
    <my-child-view></my-child-view>
  <div>-- child view ends --</div>`

次のフックは、子ビュー内の値の変更、つまり @ViewChild で装飾されたプロパティを使用して子ビューを照会することによってのみ到達できる変更に基づいてアクションを実行します。

AfterViewComponent (class excerpts)

export class AfterViewComponent implements  AfterViewChecked, AfterViewInit {
  private prevHero = '';

  // Query for a VIEW child of type `ChildViewComponent`
  @ViewChild(ChildViewComponent) viewChild: ChildViewComponent;

  ngAfterViewInit() {
    // viewChild is set after the view has been initialized
    this.logIt('AfterViewInit');
    this.doSomething();
  }

  ngAfterViewChecked() {
    // viewChild is updated after the view has been checked
    if (this.prevHero === this.viewChild.hero) {
      this.logIt('AfterViewChecked (no change)');
    } else {
      this.prevHero = this.viewChild.hero;
      this.logIt('AfterViewChecked');
      this.doSomething();
    }
  }
  // ...
}

単方向データフロールールに従う

ヒーロー名が10文字を超えると、doSomething() メソッドが画面を更新します。

AfterViewComponent (doSomething)

// This surrogate for real business logic sets the `comment`
private doSomething() {
  let c = this.viewChild.hero.length > 10 ? `That's a long name` : '';
  if (c !== this.comment) {
    // Wait a tick because the component's view has already been checked
    this.logger.tick_then(() => this.comment = c);
  }
}

なぜ doSomething() メソッドはコメントを更新する前にチェックを待つのでしょう?

Angularの単方向データフロールールは、作成後のビューの更新を禁止します。これらのフックは、コンポーネントのビューが作成された後に起動します。

フックがコンポーネントのデータバインドされたコメントプロパティをすぐに更新した場合、Angularはエラーをスローします。(実際に試してみてください!)

LoggerService.tick_then() は、ブラウザの JavaScript サイクルの1回分のログ更新を延期します。

AfterView の実際の動作は次のとおりです。

Angularは AfterViewChecked() を頻繁に呼び出すことが多く、興味のある変更がない場合が多いことに注意してください。リーンフックメソッドを記述すると、パフォーマンスの問題を回避できます。

AfterContent

AfterContent サンプルは、Angularが外部コンテンツをコンポーネントに投入した後に Angular が呼び出す、AfterContentInit() および AfterContentChecked() フックを調べることができるサンプルです。

Content projection(コンテンツ投入)

コンテンツ投入とは、コンポーネントの外部からHTMLコンテンツをインポートし、そのコンテンツを指定された場所にコンポーネントのテンプレートに挿入する方法のことを言います。

AngularJS の開発者の皆さんは、このテクニックのことを transclusion として認識されてますよね。

以前のAfterViewの例でこのバリエーションを考えてみましょう。
今回はテンプレート内に子ビューを含めるのではなく、AfterContentComponent の親からコンテンツをインポートします。親のテンプレートは次のとおりです。

AfterContentParentComponent(テンプレートを抜粋)

`<after-content>
   <my-child></my-child>
 </after-content>`

<my-child> タグは <after-content> タグの間に挟まれています。コンテンツをコンポーネントに投入する場合を除き、コンポーネントの要素タグの間にコンテンツを配置しないでください。

子コンポーネントのテンプレートも見てみましょう。

AfterContentComponent (テンプレート)

template: `
  <div>-- projected content begins --</div>
    <ng-content></ng-content>
  <div>-- projected content ends --</div>`

<ng-content> タグは、外部コンテンツのプレースホルダです。
Angularは、そのコンテンツをどこに挿入するかを指示します。この場合、投入されるコンテンツは親からの <my-child> です。

コンテンツ投入の兆候は2つあります。

  • コンポーネント要素タグの間にHTMLがあるかどうか。
  • コンポーネントのテンプレートにタグが存在するかどうか。

AfterContent hooks

AfterContent フックは AfterView フックに似ています。重要な違いは、子コンポーネントにあります。

  • AfterView フックは、コンポーネントのテンプレート内に要素タグが表示される子コンポーネントである ViewChildren に関係します。 -AfterContent フックは、Angularがコンポーネントに投影した子コンポーネントであるContentChildren に関するものです。

次の AfterContent フックは、@ContentChild で修飾されたプロパティを使用してそれらを照会することによってのみアクセスできるコンテンツ子の値の変更に基づいてアクションを実行します。

AfterContentComponent (class を抜粋)

export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
  private prevHero = '';
  comment = '';

  // Query for a CONTENT child of type `ChildComponent`
  @ContentChild(ChildComponent) contentChild: ChildComponent;

  ngAfterContentInit() {
    // contentChild is set after the content has been initialized
    this.logIt('AfterContentInit');
    this.doSomething();
  }

  ngAfterContentChecked() {
    // contentChild is updated after the content has been checked
    if (this.prevHero === this.contentChild.hero) {
      this.logIt('AfterContentChecked (no change)');
    } else {
      this.prevHero = this.contentChild.hero;
      this.logIt('AfterContentChecked');
      this.doSomething();
    }
  }
  // ...
}

AfterContent で単方向フローの心配はいりません

このコンポーネントの doSomething() メソッドは、コンポーネントのデータバインドされたコメントプロパティを直ちに更新します。待つ必要はありません。

Angularは、どちらかの AfterViewフックを呼び出す前に、両方の AfterContent フックを呼び出すことを思い出してください。 Angularは、このコンポーネントの構成を完了する前に投入されたコンテンツの構成を完了します。

ホストビューを変更するには、AfterContent...AfterView... フックの間に小さなウィンドウがあります。

7
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5