続:AngularでChangeDetectionによるパフォーマンスチューニング

angular

前回(AngularでChangeDetectionによるパフォーマンスチューニング)は、ChangeDetectionStrategy.OnPushによるチューニングについて解説しましたが今回はその続きです。

@Input()などで親コンポーネントから値を受け取るケースでは注意が必要です。

プリミティブ値の場合

@Input()で渡す値がプリミティブ値の場合は問題なく特に気にする必要はありません。

サンプル

@Component({
  selector: 'child1',
  template: `
    <p>{{message}}</p>
  `,
})
export class Child1 {
  @Input() message: string;
}
 
@Component({
  selector: 'child2',
  template: `
    <p>{{message}}</p>
  `,
  changeDetection:ChangeDetectionStrategy.OnPush
})
export class Child2 {
  @Input() message: string;
}
 
@Component({
  selector: 'app',
  template: `<div>
    <p><input type="text" [(ngModel)]="message"></p>
    <child1 [message]="message"></child1>
    <child2 [message]="message"></child2>
    </div>`
})
export class App {
  message:string;
}

サンプルでは親コンポーネントで入力された値を子コンポーネントに引き渡しています。ChangeDetectionStrategy.OnPushの有無に関わらず子コンポーネントは正常に描画されています。

オブジェクトの場合

@Input()で渡す値がオブジェクトになるとオブジェクトの内容が変化しても、ChangeDetectionStrategy.OnPushを指定したコンポーネントには変更内容が反映されません。

サンプル

@Component({
  selector: 'child1',
  template: `
    <p>{{message.name}}</p>
  `,
})
export class Child1 {
  @Input() message: {name: string};
}
 
@Component({
  selector: 'child2',
  template: `
    <p>{{message.name}}</p>
  `,
  changeDetection:ChangeDetectionStrategy.OnPush
})
export class Child2 {
  @Input() message: {name: string};
}
 
@Component({
  selector: 'app',
  template: `<div>
    <p><input type="text" [(ngModel)]="message.name"></p>
    <child1 [message]="message"></child1>
    <child2 [message]="message"></child2>
    </div>`
})
export class App {
  message: {name: string} = {name:null};
}

このサンプルではmessageオブジェクトのnameプロパティが更新されていますが、ChangeDetectionStrategy.OnPushを指定したコンポーネントは変更の感知ができていません。

これは、オブジェクトの参照自体が変更されていないためAngularが@Input()で受け取っている値が変更されている事を検知できないためです。

Observableで値を更新

これを回避するためには@Input()で渡す値をオブジェクトではなくObservableを引き渡すように変更してsubscribeで変更を検知して、ChangeDetectorRef.markForCheck();を実行することでChangeDetectionStrategy.OnPushを指定したコンポーネントには変更内容を反映させることができます。

サンプル

@Component({
  selector: 'child1',
  template: `
    <p>{{message.name}}</p>
  `,
})
export class Child1 {
  @Input() message: {name: string};
}
  
@Component({
  selector: 'child2',
  template: `
    <p>{{message.name}}</p>
  `,
  changeDetection:ChangeDetectionStrategy.OnPush
})
export class Child2 {
  @Input() data: Observable<any>;
  message: {name: string} = {name:null};
  constructor(private cd: ChangeDetectorRef) {}
  ngOnInit() {
    this.data.subscribe(value => {
      console.log(value.message)
      this.message = value.message;
      this.cd.markForCheck();
    });
  }
}
  
@Component({
  selector: 'app',
  template: `<div>
    <p><input type="text" (input)="update()" [(ngModel)]="message.name"></p>
    <child1 [message]="message"></child1>
    <child2 [data]="data$"></child2>
    </div>`
})
export class App {
  message: {name: string} = {name:null};
  data$ = new BehaviorSubject({ message:this.message });
  update(){
    this.data$.next({ message:this.message });
  }
}

子コンポーネントの再描画用のObservable

巨大なオブジェクトを元にコンポーネントを構築している場合など、オブジェクトをObservableとして引き渡すのは少し面倒くさいケースというのもあります。
また、オブジェクトの変更のタイミングが必ずしも子コンポーネントの更新のタイミングとは限りません。

そういったケースでは子コンポーネントの更新用のObservableを用意して、手動で更新することで任意のタイミングで子コンポーネントの再描画を制御することも可能です。

サンプル

@Component({
  selector: 'child1',
  template: `
    <p>{{message.name}}</p>
  `,
})
export class Child1 {
  @Input() message: {name: string};
}
  
@Component({
  selector: 'child2',
  template: `
    <p>{{message.name}}</p>
  `,
  changeDetection:ChangeDetectionStrategy.OnPush
})
export class Child2 {
  @Input() message: {name: string};
  @Input() update: Observable<any>;
    
  constructor(private cd: ChangeDetectorRef) {}
  ngOnInit() {
    this.update.subscribe(value => {
      this.cd.markForCheck();
    });
  }
}
 
@Component({
  selector: 'app',
  template: `<div>
    <p><input type="text" [(ngModel)]="message.name"></p>
    <p><input type="button" value="update" (click)="update()" /></p>
    <child1 [message]="message"></child1>
    <child2 [message]="message" [update]="update$"></child2>
    </div>`
})
export class App {
  message: {name: string} = {name:null};
  _counter:number=0;
  update$ = new BehaviorSubject({ counter:this._counter });
  update(){
    this.update$.next({ counter:++this._counter});
  }
}

サンプルではmessageの更新のタイミングではなくupdateボタンが押下されたタイミングで子コンポーネントの描画を行なっています。

関連エントリー

SystemJSでAngular 2の環境を構築する

スポンサードリンク

«AngularでChangeDetectionによるパフォーマンスチューニング | メイン | Angularによるスライドアニメーション»