9
1

More than 3 years have passed since last update.

Angular OnPush Component注意すべきなどこる: 続き

Posted at

前日の記事でAngular OnPush Componentでいろいろ注意すべきなどころを書きました、この記事がさらにいくつ普段あまり使われていないケースを説明したいと思います。

  1. ケース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 になります。

image.png

なので、このHostViewのdetectChanges()をコールして、OnPush ComponentがまだDirtyではなくため、画面が更新されません。

  1. ケース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と似てるですが、テストにもこの現象が有ります。

  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に所属していますため。

  1. ケース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の原因があります、これを説明すると、すごく長文になりしょうですので、纏めると、

  1. Directiveを設計するとき、DirectiveがHostElementがないので、DirectiveのHooksとかを一個PlaceholderのHostViewに置く設計になりました。
  2. ComponentもDirectiveなので(ComponentがViewがあるDirective)、設計を統一するため、ComponentにもHostViewを持ちました。
  3. 普通のComponentの場合、わざわざ新しいHostViewを作る必要がなく、Parent Component ViewをHostViewとして利用する
  4. BootStrap/Dynamic Componentの場合、作成したとき、親が分からないため、新しく一個HostViewを作成されました。

ということになりました。

このHostViewが開発者意識させたくないものなので、開発者が直接触るケースがないはずですが、上記のケースでちょっとわかりにくいことが発生しました。

今これらの問題を解決するため、大幅にChange Detectionのデータ構造・ロジックを更新することが難しくて、これから、

  1. Documentを強化して、これらのケースとWork Around案を明確する
  2. 将来てきには、HostViewをなくす設計を検討する

ということになります。
今わたしのほうで1番目を対応中ですが、皆さんがこれらの問題をあうとき、この記事が参考になればいいと思います。

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

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