皆さん、こんにちは、まだ年に一度の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 を持っています。
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に教えることが必要になります。
以上です、どうもありがとうございました。