42

このエラーについていくつか読んで調査しましたが、私の状況に対する正しい答えはわかりません。enableProdMode()開発モードでは、変更検出が 2 回実行されることは理解していますが、問題を隠すために使用するのは気が進まない.

これは、div の幅が拡大するにつれて、テーブル内のセルの数が増加する必要がある簡単な例です。(div の幅は画面幅だけの関数ではないため、@Media は簡単に適用できないことに注意してください)

私の HTML は次のようになります (widget.template.html):

<div #widgetParentDiv class="Content">
<p>Sample widget</p>
<table><tr>
   <td>Value1</td>
   <td *ngIf="widgetParentDiv.clientWidth>350">Value2</td>
   <td *ngIf="widgetParentDiv.clientWidth>700">Value3</td>
</tr></table>

これ自体は何もしません。これは、変更検出が発生する原因が何もないためだと思います。ただし、最初の行を次のように変更し、空の関数を作成して呼び出しを受け取ると、機能し始めますが、「チェックされた後に式が変更されました」エラーが発生することがあります。

<div #widgetParentDiv class="Content">
   gets replaced with
      <div #widgetParentDiv (window:resize)=parentResize(10) class="Content">

私の最善の推測では、この変更により変更検出がトリガーされ、すべてが応答を開始しますが、幅が急速に変化すると、div の幅を変更するよりも前の変更検出の反復が完了するのに時間がかかるため、例外がスローされます。

  1. 変更検出をトリガーするためのより良い方法はありますか?
  2. 変更検出が確実に行われるようにするには、関数を介してサイズ変更イベントをキャプチャする必要がありますか?
  3. #widthParentDiv を使用して div の幅にアクセスできますか?
  4. より良い全体的な解決策はありますか?

私のプロジェクトの詳細については、この同様の質問を参照してください。

ありがとう

4

5 に答える 5

68

divこの問題を解決するには、サイズ変更イベントごとにコンポーネント プロパティのサイズを取得して保存し、そのプロパティをテンプレートで使用するだけです。このようにして、変更検出の 2 回目のラウンドが開発モードで実行されるときに、値は一定のままになります。

また、テンプレートに@HostListener追加するのではなく、使用することをお勧めします。への参照を取得するために(window:resize)使用します。そして、ライフサイクル フックを使用して初期値を設定します。@ViewChilddivngAfterViewInit()

import {Component, ViewChild, HostListener} from '@angular/core';

@Component({
  selector: 'my-app',
  template: `<div #widgetParentDiv class="Content">
    <p>Sample widget</p>
    <table><tr>
       <td>Value1</td>
       <td *ngIf="divWidth > 350">Value2</td>
       <td *ngIf="divWidth > 700">Value3</td>
      </tr>
    </table>`,
})
export class AppComponent {
  divWidth = 0;
  @ViewChild('widgetParentDiv') parentDiv:ElementRef;
  @HostListener('window:resize') onResize() {
    // guard against resize before view is rendered
    if(this.parentDiv) {
       this.divWidth = this.parentDiv.nativeElement.clientWidth;
    }
  }
  ngAfterViewInit() {
    this.divWidth = this.parentDiv.nativeElement.clientWidth;
  }
}

うまくいかないのは残念です。我々が得る

チェック後に表情が変わっています。以前の値: 「false」。現在の値: 「真」。

エラーは式について不平を言っていますNgIf-最初に実行するdivWidthと0になり、次にngAfterViewInit()実行して値を0以外に変更し、2回目の変更検出が実行されます(devモードで)。ありがたいことに、簡単で既知の解決策があります。これは 1 回限りの問題であり、OP のような継続的な問題ではありません。

  ngAfterViewInit() {
    // wait a tick to avoid one-time devMode
    // unidirectional-data-flow-violation error
    setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  }

1 ティックを待機するこの手法は、https ://angular.io/docs/ts/latest/cookbook/component-communication.html#!#parent-to-view-child で文書化されていることに注意してください。

多くの場合、これらのメソッドはコンポーネントのビューが構成された後に呼び出されるため、このトリックを使用する必要がありますngAfterViewInit()ngAfterViewChecked()setTimeout()

ここに作業がありplunkerます。


これを改善することができます。サイズ変更イベントが発生するたびにではなく、たとえば 100 ~ 250 ミリ秒ごとに Angular 変更検出が実行されるように、サイズ変更イベントを調整する必要があると思います。これにより、ユーザーがウィンドウのサイズを変更しているときにアプリの動作が遅くなるのを防ぐことができます。現在、サイズ変更イベントごとに変更検出が実行されるためです (開発モードで 2 回)。これは、前のプランカーに次のメソッドを追加することで確認できます。

ngDoCheck() {
   console.log('change detection');
}

オブザーバブルはイベントを簡単に調整できるため@HostListener、サイズ変更イベントにバインドする代わりに、オブザーバブルを作成します。

Observable.fromEvent(window, 'resize')
   .throttleTime(200)
   .subscribe(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth );

これは機能しますが...それを試しているうちに、非常に興味深いことがわかりました...サイズ変更イベントを抑制しても、サイズ変更イベントが発生するたびにAngularの変更検出が実行されます。つまり、スロットリングは変更検出の実行頻度には影響しません。(Tobias Bosch はこれを確認しました: https://github.com/angular/angular/issues/1773#issuecomment-102078250。)

イベントがスロットル時間を経過した場合にのみ変更検出を実行したい。そして、このコンポーネントで実行する変更検出だけが必要です。解決策は、Angular ゾーンの外側にオブザーバブルを作成し、サブスクリプション コールバック内で変更検出を手動で呼び出すことです。

constructor(private ngzone: NgZone, private cdref: ChangeDetectorRef) {}
ngAfterViewInit() {
  // set initial value, but wait a tick to avoid one-time devMode
  // unidirectional-data-flow-violation error
  setTimeout(_ => this.divWidth = this.parentDiv.nativeElement.clientWidth);
  this.ngzone.runOutsideAngular( () =>
     Observable.fromEvent(window, 'resize')
       .throttleTime(200)
       .subscribe(_ => {
          this.divWidth = this.parentDiv.nativeElement.clientWidth;
          this.cdref.detectChanges();
       })
  );
}

ここに作業がありplunkerます。

plunker に、counterライフサイクル フックを使用してすべての変更検出サイクルをインクリメントする を追加しましたngDoCheck()。このメソッドが呼び出されていないことがわかります。カウンター値はサイズ変更イベントで変化しません。

detectChanges()このコンポーネントとその子に対して変更検出を実行します。ルート コンポーネントから変更検出を実行する (つまり、完全な変更検出チェックを実行する) 場合は、ApplicationRef.tick()代わりに使用します (これは plunker でコメント化されています)。が呼び出されるtick()ことに注意してください。ngDoCheck()


これは素晴らしい質問です。さまざまなソリューションを試すのに多くの時間を費やし、多くのことを学びました。この質問を投稿していただきありがとうございます。

于 2016-08-13T23:21:29.307 に答える
17

これを解決するために使用した他の方法:

import { Component, ChangeDetectorRef } from '@angular/core';


@Component({
  selector: 'your-seelctor',
  template: 'your-template',
})

export class YourComponent{

  constructor(public cdRef:ChangeDetectorRef) { }

  ngAfterViewInit() {
    this.cdRef.detectChanges();
  }
}
于 2017-09-21T20:31:17.457 に答える
0

Mark Rajcok がすばらしい回答をしてくれました。より単純なバージョン (スロットリングなし) は次のようになります。

ngAfterViewInit(): void {
    this.windowResizeSubscription = fromEvent(window, 'resize').subscribe(() => this.onResize())
    this.onResize() // to initialize before any change
  }

onResize() {
    this.width = this.elementRef.nativeElement.getBoundingClientRect().width;
    this.changeDetector.detectChanges();
  }
于 2021-10-28T20:42:29.870 に答える