OnPush 注意するべきなどころ

皆さん、こんにちは、まだ年に一度の[Angularアドベントカレンダー](https://qiita.com/advent-calendar/2020/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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109004/5ff225fb-3335-99b5-5e60-d2dd12ba8f29.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に教えることが必要になります。

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