23
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OnPush 注意するべきなどころ

Last updated at Posted at 2020-12-12

皆さん、こんにちは、まだ年に一度のAngularアドベントカレンダーになりました、今回が OnPush について、いろいろ話したいと思います。

私が Jia Li と申します、Angular の Zone.js というライブラリを 4 年間開発して、今が Zone.js の Code Owner と Angular Collaborator として Angular の Contribution をやっています。

OnPush Component

Angular を開発するとき、性能向上するため、Component の ChangeDetectionStrategy を OnPush をすることが一つの有力な対策です。まず OnPush の仕組みを説明します。

例えば、下記のサンプルをみてください。

@Component({
  selector: 'app-onpush',
  template: `<button (click)="click()"></button>{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
  @Output() output = new EventEmitter();
  click() {}
}

@Component({
  selector: 'app-root',
  template: `<app-onpush [input]="inputData" (output)="fromPush($event)"></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  inputData = 'pushInput';
  fromPush() {}
}

Angular 内部で下記のような View Tree を持っています。

Screen Shot 2020-12-12 at 16.48.33.png

Angular の Change Detection が実行されるとき、RootView から子供の TreeNode に全部 Visit して、変更があるかどうかを探します。

  • ChangeDetection.Strategy が Default のとき、条件なく該当 Component の変更探知を実行します。
  • ChangeDetection.Strategy が OnPush のとき、ある条件に満足しないと変更探知をしません。

OnPushのとき、変更探知をするかどうかの条件が下記になります。

OnPush ComponentがDirtyするとき

そして、いつDirtyになりますかというと:

  • 自動てきにDirtyになるパターン Templateで宣言された要素が動きがあるかどうかということです
  • 手動てきにDirtyになるパターン markDirty()/markForCheck()などを手動でコールする

このArticleが主に自動のパターンを説明したいと思います。

Templateに宣言された要素が:

  • Input
  • Output
  • EventListener
  • AsyncPipe

ここで一番重要なポイントがこれらの要素がTemplateに書かないと自動的にOnPush ComponentをDirtyになれません。

  • Input
@Component({
  selector: 'app-onpush',
  template: `{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
}

@Component({
  selector: 'app-root',
  template: `<app-onpush [input]="inputData"></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  inputData = 'pushInput';
}

Inputの場合、Templateに宣言する意味がOnPush ComponentのElementがいるTemplate(<app-onpush [input]="inputData"></app-onpush>)でproperty binding/interpolation を宣言する意味です。このように宣言したら、AppComponentでinputDataを変更したら、OnPush Componentが自動的にDirtyになって、Change Detectionの変更探知の対象になります。

もし宣言しない場合、

@Component({
  selector: 'app-onpush',
  template: `{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;

  ngOnInit() {
    // use setTimeout since ngOnInit is in the change detection because
    // this is a part of the component create phase
    setTimeout(() => {
      this.input = 'updated value';
    });
  }
}

@Component({
  selector: 'app-root',
  template: `<app-onpush></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
}

このとき、inputがOnPush Componentで@Inputとして定義しても、Templateで宣言していません。そしたら、たとえば、setTimeoutなどの処理でこのinputの値を更新しても、画面が更新されません、つまりOnPush ComponentがDirtyになっていません。

その原因が宣言されたとき、AngularがTemplateをCompileするとき、<app-onpush [input]="inputData" をみて、このinput propertyがOnPush Componentの@Inputということが分かって、そしたら、inputを変更するInstructionコードでOnPush ComponentをDirty化にする処理も入れました。なので、宣言しないと、AngularがTemplateをCompileするときこの関係がわからないので、自動てきにDirtyすることができません。

  • Output
@Component({
  selector: 'app-onpush',
  template: `<button (click)="click()"></button>{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
  @Output() output = new EventEmitter();
  click() {
    this.output.emit('updated');
  }
}

@Component({
  selector: 'app-root',
  template: `<app-onpush [input]="inputData" (output)="fromPush($event)"></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  inputData = 'pushInput';
  fromPush(value) {}
}

Outputの場合、Templateに宣言する意味がOnPush ComponentのElementがいるTemplate(<app-onpush (output)="fromPush($event)"></app-onpush>)でevent binding を宣言する意味です。このように宣言したら、OnPush ComponentでEventがEmitされたら、OnPush Componentが自動的にDirtyになって、Change Detectionの変更探知の対象になります。

もし宣言しない場合、

@Component({
  selector: 'app-onpush',
  template: `{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @Input() input;
  @Output() output = new EventEmitter();

  ngOnInit() {
    // use setTimeout since ngOnInit is in the change detection because
    // this is a part of the component create phase
    setTimeout(() => {
     this.output.emit('updated');
    });
  }
}

@Component({
  selector: 'app-root',
  template: `<app-onpush></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
}

このとき、outputがOnPush Componentで@Outputとして定義しても、Templateで宣言していません。そしたら、たとえば、setTimeoutなどの処理でこのoutputでなにか値をEmitしても、画面が更新されません、つまりOnPush ComponentがDirtyになっていません。

その原因が宣言されたとき、AngularがTemplateをCompileするとき、<app-onpush (output)="fromPush($event)" をみて、fromPush というEvent Bindingの関数をWrapして、この関数が所属するComponentがもしOnPushの場合、Dirty化にする処理も入れました。なので、宣言しないと、AngularがTemplateをCompileするときこの関係がわからないので、自動てきにDirtyすることができません。

  • Event Listener
@Component({
  selector: 'app-onpush',
  template: `<button (click)="click()"></button>{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  click() {
    this.input = 'clicked';
  }
}

@Component({
  selector: 'app-root',
  template: `<app-onpush></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
  inputData = 'pushInput';
}

EventListenerの場合、Outputと似ています、OutputがOnPush ComponentのElementがいるTemplateで宣言しますが、EventListenerがOnPush Component自分のTemplateで宣言する意味です。このように宣言したら、EventがTriggerされたら、OnPush Componentが自動的にDirtyになって、Change Detectionの変更探知の対象になります。

もし宣言しない場合、

@Component({
  selector: 'app-onpush',
  template: `<button #btn>Click</button>{{input}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  @ViewChild('btn') btn;
  ngOnInit() {
    this.btn.nativeElement.addEventListener('click', click);
  }
}

@Component({
  selector: 'app-root',
  template: `<app-onpush></app-onpush>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
}

このとき、clickがOnPush Componentでclick event listenerとして登録しても、Templateで宣言していません。Buttonをクリックしても、画面が更新されません、つまりOnPush ComponentがDirtyになっていません。

その原因が宣言されたとき、AngularがTemplateをCompileするとき、<button (click)="click()">Click</button>" をみて、click というEvent Bindingの関数をWrapして、この関数が所属するComponentがもしOnPushの場合、Dirty化にする処理も入れました。なので、宣言しないと、AngularがTemplateをCompileするときこの関係がわからないので、自動てきにDirtyすることができません。

  • Async Pipe
@Component({
  selector: 'app-onpush',
  template: `{{data$ | async}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPushComponent {
  data$: Observable<any>;
}

Async PipeがとくにOnPush Componentと関係がありません、Async Pipeの仕組みが新しい値がくるとき、自動的にmarkForCheck()を呼び出して、該当のComponentとAncestorComponentを全部Dirty化することになります。

@Pipe({name: 'async', pure: false})
export class AsyncPipe implements OnDestroy, PipeTransform {
obs$ = this.observablesToSubscribeSubject
  .pipe(
    distinctUntilChanged(ɵlooseIdentical),
    switchAll(),
    distinctUntilChanged(),
    tap(v => { this.value = v; this.ref.markForCheck(); })
  );

纏め

つまり、OnPush Componentを利用するとき、必ずAngular Compilerをわかれるように、Templateに宣言して、これらの要素が変更可能ということをAngularに教えることが必要になります。

以上です、どうもありがとうございました。

23
9
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
23
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?