Angular2 時刻更新例外対策
Angular2における「expression has changed after it was checked」例外の解説
問題
Angular2のコンポーネント内で、現在の時刻に依存するプロパティを使用すると、しばしば「expression has changed after it was checked」という例外が発生します。これは、Angularの変更検出メカニズムが、プロパティの値が変更されたと検出した後に、その値が再び変更されたことを検出したためです。
原因
- 非同期処理
現在の時刻を取得する操作が非同期処理の場合、Angularの変更検出サイクルが完了する前に、時刻が更新されることがあります。 - setTimeout/setInterval
これらの関数を使用して、定期的に時刻を更新する場合、Angularの変更検出サイクルの外で更新が行われるため、例外が発生します。
解決策
-
ChangeDetectorRef
ChangeDetectorRef
を使用することで、手動で変更検出をトリガーできます。- コンポーネントのコンストラクタで
ChangeDetectorRef
を注入し、必要に応じてdetectChanges()
メソッドを呼び出します。
import { Component, ChangeDetectorRef } from '@angular/core'; @Component({ selector: 'app-my-component', templateUrl: './my-component.html', styleUrls: ['./my-component.css'] }) export class MyCompone nt { currentTime: Date; constructor(private changeDetectorRef: ChangeDetectorRef) { this.currentTime = new Date(); } updateCurrentTime() { this.currentTime = new Date(); this.changeDetectorRef.detectChanges(); } }
-
Observable
- RxJSの
Observable
を使用して、時刻の更新を監視し、変更検出をトリガーできます。 Observable.interval()
を使用して、一定間隔で時刻を取得し、subscribe()
を使用して値を更新します。
import { Component, OnInit } from '@angular/core'; import { Observable, interval } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selecto r: 'app-my-component', templateUrl: './my-component.html', styleUrls: ['./my-component.css'] }) export class MyComponent implements O nInit { currentTime: Date; constructor() {} ngOnInit() { this.currentTime = new Date(); const intervalObservable = interval(1000).pipe(map(() => new Date())); intervalObservable.subscribe(time => { this.currentTime = time; }); } }
- RxJSの
ChangeDetectorRefを使用する
import { Component, ChangeDetectorRef } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyCompone nt {
currentTime: Date;
constructor(private changeDetectorRef: ChangeDetectorRef) {
this.currentTime = new Date();
}
updateCurrentTime() {
this.currentTime = new Date();
this.changeDetectorRef.detectChanges();
}
}
updateCurrentTime()
メソッド内で、時刻を更新した後、detectChanges()
メソッドを呼び出して変更検出をトリガーします。- 解説
コンポーネントのコンストラクタでChangeDetectorRef
を注入します。
Observableを使用する
import { Component, OnInit } from '@angular/core';
import { Observable, interval } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selecto r: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements O nInit {
currentTime: Date;
constructor() {}
ngOnInit() {
this.currentTime = new Date();
const intervalObservable = interval(1000).pipe(map(() => new Date()));
intervalObservable.subscribe(time => {
this.currentTime = time;
});
}
}
subscribe()
メソッド内で、取得した時刻をコンポーネントのプロパティに更新します。- 解説
RxJSのObservable
を使用して、1秒ごとに時刻を取得します。
AsyncPipeを使用する
import { Component, OnInit } from '@angular/core';
import { interval } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({
selector: 'ap p-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent implements O nInit {
currentTime$: Observable<Date>;
constructor() {}
ngOnInit() {
this.currentTime$ = interval(1000).pipe(map(() => new Date()));
}
}
interval
を使用して1秒ごとに時刻を取得し、map
を使用してDate
オブジェクトに変換します。- 解説
AsyncPipe
を使用して、テンプレート内でObservable
を直接バインドします。
OnPush変更検出戦略
- 適用方法
コンポーネントのデコレータにchangeDetection: ChangeDetectionStrategy.OnPush
を指定します。
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component. html',
styleUrls: ['./my-component.css'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MyC omponent {
currentTime: Date;
// ...
}
markForCheck()メソッド
- 適用方法
ChangeDetectorRef
を注入し、必要に応じてmarkForCheck()
を呼び出します。
import { Component, ChangeDetectorRef, OnInit } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyCompone nt implements OnInit {
currentTime: Date;
constructor(private changeDetectorRef: ChangeDetectorRef) {}
ngOnInit() {
// ...
this.changeDetectorRef.markForCheck();
}
}
immutable.jsライブラリ
- 適用方法
immutable.js
ライブラリをインストールし、コンポーネント内で使用します。
import { Component } from '@angular/core';
import { Map } from 'immutable';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyComponent {
currentTime: Map<string, any>;
// ...
}
NgZoneのrunOutsideAngular()メソッド
- 適用方法
NgZone
を注入し、runOutsideAngular()
内で非同期処理を実行します。
import { Component, NgZone } from '@angular/core';
@Component({
selector: 'app-my-component',
templateUrl: './my-component.html',
styleUrls: ['./my-component.css']
})
export class MyCompone nt {
currentTime: Date;
constructor(private ngZone: NgZone) {}
updateCurrentTime() {
this.ngZone.runOutsideAngular(() => {
// 非同期処理
setTimeout(() => {
this.currentTime = new Date();
this.ngZone.run(() => {
// Angularの変更検出をトリガー
});
}, 1000);
});
}
}
angular typescript time