まずはじめに
Angularで開発している中で、以下のような事象が発生した
- Google Map上でマーカーをクリックした際に、コンポーネントのビューが更新されない
=> Angularの「変更検知」に起因していることが後ほどわかる
そもそもAngularの変更検知とは?
Angularアプリケーションの状態を更新する要因となるのは主に以下の3つ。これらが発生すると、Angularは状態が更新されたとみなし、ビューを更新する。
- Events - click, change, input, submitのようなユーザーイベント
- XMLHttpRequests - fetchなどの非同期処理
- Timers - setTimeout(), setInterval()など
今回のケースはclickイベントであり、一見問題ないように思われる。
Angularの変更検知の仕組み
Angularの各コンポーネントには、Change Detectorというものが備わっており、イベントや非同期処理などの発生を検知することができるようになっており、変更のたびにビューを更新することができるようになっている。
Change Detectorはコンポーネントと同様ツリー構造になっており、親から子、子から孫へと変更を伝えていく。
変更検知の要 NgZone(Zone.js)
Change Detectorの実態はZone.jsと呼ばれる非同期処理のユーティリティライブラリ。
AngularはこのZone.jsをNgZoneとして内部に備えており、イベントや非同期処理などをモンキーパッチしている。そしてAngularはこのNgZoneの中でコンポーネントのコードを実行している。このような仕組みで変更の検知を行なっている。
コードで確認すると
ApplicationRefクラスに以下のコードがあり、ブートストラップ時に呼び出される。
// zone.jsでパッチしているコードの処理が完了した時に通知し、
// 変更検知を行うthis.tick()を呼び出す。
this._zone.onMicrotaskEmpty.subscribe({
next: () => {
this._zone.run(() => {
this.tick();
});
}
});
// 全てのコンポーネントに対して変更検知のためのdetectChanges()を呼び出す。
tick(): void {
this._views.forEach(
(view) => view.detectChanges()
);
}
最初のGoogle Mapのケースはどういうことだったか?
Google Map上のクリックはAngularの外で発生したものであり、NgZoneにパッチされていなかった。
そのためコンポーネントでの変更検知の対象外となり、いくらクリックしてもビューの更新がなされなかった。
解決策はあるのか?
NgZoneのrun()
メソッドを使用することで、明示的にNgZoneの内部でコードを実行することができるようになり、Angularに変更検知行させることが可能となる。
gMarker.addListener('click', () => {
this.ngZone.run(() => {
this.markerClick.next(marker);
});
});
逆に変更検知させたくない場合
NgZoneのrunOutsideAngular()
メソッドの中で実行したコードは変更検知の対象外となる。
this.ngZone.runOutsideAngular(
() => this.hogeService.fuga()
);
こうすることで、NgZoneの外側でコードを実行することができ、Angularの変更検知を引き起こさないようにすることも可能。
まとめ
Angularの変更検知は通常はほとんど意識する必要はないため、変更検知周りでハマった時に苦労することが多い。
しかし実態はZone.jsであるということがわかれば、そんなに恐れることはない。
詳しく調べて見るとAngularの理解が深まると思う。
参考にしたもの
- AngularとZone.jsとテストの話 - Qiita
- 日本語訳:Angular 2 Change Detection Explained - Qiita
- Angularでイベントから無駄にChange Detectionを走らせないためにすべきこと
- Zones in Angular by thoughtram
- application_ref.ts - angular/angular