前日の記事でAngular OnPush Componentでいろいろ注意すべきなどころを書きました、この記事がさらにいくつ普段あまり使われていないケースを説明したいと思います。
- ケース1:
componentRef.changeDetectorRef.detectChanges()
is confusing.
@component({
selector: 'dynamic',
template: `{{name}}`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Dynamic {
name = 'initial name';
}
@component({
selector: 'my-app',
template: `
<b>Expected</b>: "initial name" changes to "changed name" after 2s<br>
<b>Actual</b>:<br>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
constructor(private _vcRef: ViewContainerRef,
private _cfr: ComponentFactoryResolver, private _injector: Injector) {}
ngOnInit() {
const cmptRef = this._cfr.resolveComponentFactory(Dynamic).create(this._injector);
this._vcRef.insert(cmptRef.hostView);
setTimeout(() => {
cmptRef.instance.name = 'changed name';
cmptRef.changeDetectorRef.detectChanges(); // this will not update the DOM.
// cmptRef.injector.get(ChangeDetectorRef).detectChanges(); // this will update the DOM.
}, 2000);
}
}
このケースがDynamic
のOnPush Componentを追加して、このcomponentのcmptRef.changeDetectorRef.detectChanges()
をコールしても、画面が更新されないという現象になります。
そして、もしcmptRef.injector.get(ChangeDetectorRef).detectChanges();
で実行されたら、画面が更新されました。
その原因がcmptRef.changeDetectorRef
がこのOnPush ComponentのchangeDetectorRefではなく、そのComponentを格納するPlaceholderのHostViewのchangeDetectorRef になります。
なので、このHostViewのdetectChanges()
をコールして、OnPush ComponentがまだDirtyではなくため、画面が更新されません。
- ケース2:
ComponentFixture.detectChanges()
is confusing
const myComp = TestBed.createComponent(OnPushComponent);
myComp.componentInstance.abc = 123;
myComp.detectChanges() // Does not work
myComp.componentRef.injector.get(ChangeDetectorRef).detectChanges(); // This will work.
ケース1と似てるですが、テストにもこの現象が有ります。
- ケース3:
ngDoCheck()
が実行されましたが, 実際ChangeDetection
が実行されていません.
@Component({
selector: "onpush",
template: `
onpush: {{ name }} <br />
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPush implements DoCheck {
name = "initial name";
ngDoCheck() {
console.log("docheck onpush");
}
}
@Component({
selector: "my-app",
template: `
<onpush></onpush>
<button (click)="onClick()">Update onpush</button>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
@ViewChild(OnPush) onPushComp: OnPush;
constructor() {}
onClick() {
this.onPushComp.name = 'new name';
}
}
このケースの場合、OnPushが更新されないのは正しいですが、ngDoCheck
が実行されたのが変です。
その理由がHostView
の存在です、実際はComponentのLifecycle hooksが親のViewに所属していますため。
- ケース4:
dev mode
で、OnPush ComponentがCheckNoChanges()
が実行されていません、つまりどのようなパターンでもExpressionChangedAfterItHasBeenCheckedError
になれません。
import {
Component,
NgModule,
ViewChild,
ChangeDetectionStrategy,
DoCheck,
Input, AfterViewInit
} from "@angular/core";
import { BrowserModule } from "@angular/platform-browser";
@Component({
selector: "onpush",
template: `
onpush: {{ name }} <br />
{{ input }}
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class OnPush implements AfterViewInit {
name = 'initial name';
ngAfterViewInit() {
this.name = 'updated name';
}
}
@Component({
selector: "my-app",
template: `
<b>Expected</b>: "initial name" changes to "new name" after button click<br />
<b>Actual</b>:<br />
<onpush></onpush>
`,
changeDetection: ChangeDetectionStrategy.Default
})
export class AppComponent {
@ViewChild(OnPush) onPushComp: OnPush;
constructor() {}
}
その原因が今AngularがExpressionChangedAfterItHasBeenCheckedError
を探知するため、開発モードで2回Change Detectionを実行しました。OnPushの場合、1回目でDirtyフラグをリセットして、2回目がDirtyではない状態になって、CheckNoChangesの処理が実行されなくなりました。
では、なぜHostView
という概念が存在しますか?ちょっといろいろなLegacyの原因があります、これを説明すると、すごく長文になりしょうですので、纏めると、
- Directiveを設計するとき、DirectiveがHostElementがないので、DirectiveのHooksとかを一個PlaceholderのHostViewに置く設計になりました。
- ComponentもDirectiveなので(ComponentがViewがあるDirective)、設計を統一するため、ComponentにもHostViewを持ちました。
- 普通のComponentの場合、わざわざ新しいHostViewを作る必要がなく、Parent Component ViewをHostViewとして利用する
- BootStrap/Dynamic Componentの場合、作成したとき、親が分からないため、新しく一個HostViewを作成されました。
ということになりました。
このHostViewが開発者意識させたくないものなので、開発者が直接触るケースがないはずですが、上記のケースでちょっとわかりにくいことが発生しました。
今これらの問題を解決するため、大幅にChange Detectionのデータ構造・ロジックを更新することが難しくて、これから、
- Documentを強化して、これらのケースとWork Around案を明確する
- 将来てきには、HostViewをなくす設計を検討する
ということになります。
今わたしのほうで1番目を対応中ですが、皆さんがこれらの問題をあうとき、この記事が参考になればいいと思います。
以上です、どうもありがとうございました。