ホームページ  >  記事  >  ウェブフロントエンド  >  Angular での onPush 変更検出戦略をすばやく理解する

Angular での onPush 変更検出戦略をすばやく理解する

青灯夜游
青灯夜游転載
2021-09-09 19:44:362173ブラウズ

この記事では、Angular の onPush 変更検出戦略について詳しく説明します。お役に立てば幸いです。

Angular での onPush 変更検出戦略をすばやく理解する

デフォルトの変更検出戦略

デフォルトでは、Angular は変更検出に ChangeDetectionStrategy.Default 戦略を使用します。 。

デフォルトの戦略では、アプリケーションに関する事前の仮定が行われないため、ユーザー イベント、タイマー、XHR、Promise、およびその他のイベントによってアプリケーション内のデータが変更されるたびに、すべてのコンポーネントが変更検出を実行します。

これは、クリック イベントから Ajax 呼び出しから受信したデータまでのあらゆるイベントが変更検出をトリガーすることを意味します。

コンポーネントでゲッターを定義し、それをテンプレートで使用することで、これを簡単に確認できます。

@Component({
  template: `
    <h1>Hello {{name}}!</h1>
    {{runChangeDetection}}
  `
})
export class HelloComponent {
  @Input() name: string;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <hello></hello>
    <button (click)="onClick()">Trigger change detection</button>
  `
})
export class AppComponent  {
  onClick() {}
}

上記のコードを実行した後、ボタンをクリックするたびに。 Angular は変更検出ループを実行し、コンソールに 2 行の「ビューの確認」ログが表示されます。

この手法はダーティ チェックと呼ばれます。ビューを更新する必要があるかどうかを知るために、Angular は新しい値にアクセスし、それを古い値と比較して、ビューを更新する必要があるかどうかを判断する必要があります。

ここで、数千の式を含む大規模なアプリケーションがあり、Angular が各式をチェックすると、パフォーマンスの問題が発生する可能性があると想像してください。

では、コンポーネントをいつチェックするかを事前に Angular に伝える方法はあるのでしょうか?

OnPush 変更検出戦略

コンポーネントの ChangeDetectionStrategyChangeDetectionStrategy.OnPush に設定できます。

これにより、コンポーネントが @inputs() にのみ依存し、次の状況でのみチェックする必要があることが Angular に通知されます:

1. 入力 参照変更

onPush 変更検出検出戦略を設定することにより、Angular と契約して、不変オブジェクト (または後で紹介するオブザーバブル) の使用を強制します。

変更検出のコンテキストで不変オブジェクトを使用する利点は、参照が変更されたかどうかをチェックすることで、Angular がビューをチェックする必要があるかどうかを判断できることです。これは詳細な検査よりもはるかに簡単です。

オブジェクトを変更して結果を見てみましょう。

@Component({
  selector: &#39;tooltip&#39;,
  template: `
    <h1>{{config.position}}</h1>
    {{runChangeDetection}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TooltipComponent  {

  @Input() config;

  get runChangeDetection() {
    console.log(&#39;Checking the view&#39;);
    return true;
  }
}
@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config.position = &#39;bottom&#39;;
  }
}

現時点では、ボタンをクリックしてもログは表示されません。これは、Angular が次のように古い値と新しい値の参照を比較するためです。数値、ブール値、文字列、null、未定義はすべてプリミティブ型であることに注意してください。すべてのプリミティブ型は値によって渡されます。オブジェクト、配列、関数も値によって渡されますが、値は

参照アドレス

のコピーです。 したがって、このコンポーネントで変更検出をトリガーするには、このオブジェクトへの参照を変更する必要があります。

/** Returns false in our case */
if( oldValue !== newValue ) { 
  runChangeDetection();
}

オブジェクト参照を変更した後、ビューがチェックされ、新しい値が表示されていることがわかります。

2. コンポーネントまたはそのサブコンポーネントから発生するイベント

コンポーネントまたはそのサブコンポーネントでイベントがトリガーされると、コンポーネントの内部状態が更新されます。 例:

@Component({
  template: `
    <tooltip [config]="config"></tooltip>
  `
})
export class AppComponent  {
  config = {
    position: &#39;top&#39;
  };

  onClick() {
    this.config = {
      position: &#39;bottom&#39;
    }
  }
}

ボタンをクリックすると、Angular は変更検出ループを実行し、ビューを更新します。

最初に説明したように、すべての非同期 API が変更検出をトリガーすると思うかもしれませんが、そうではありません。

このルールは DOM イベントにのみ適用されることがわかります。次の API は変更検出をトリガーしません:

@Component({
  template: `
    <button (click)="add()">Add</button>
    {{count}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  add() {
    this.count++;
  }

}

まだ属性を更新していることに注意してください。そのため、次の変更検出プロセスでは、たとえば、ボタンをクリックすると、カウント値は 6 (5 1) になります。

3. 変更検出を明示的に実行する

Angular には、変更検出をトリガーする 3 つのメソッドが用意されています。

最初の 1 つは

detectChanges()

で、このコンポーネントとそのサブコンポーネントで変更検出を実行するように Angular に指示します。 <pre class="brush:js;toolbar:false;">@Component({ template: `...`, changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { count = 0; constructor() { setTimeout(() =&gt; this.count = 5, 0); setInterval(() =&gt; this.count = 5, 100); Promise.resolve().then(() =&gt; this.count = 5); this.http.get(&amp;#39;https://count.com&amp;#39;).subscribe(res =&gt; { this.count = res; }); } add() { this.count++; }</pre>2 つ目は

ApplicationRef.tick()

で、アプリケーション全体に対して変更検出を実行するように Angular に指示します。 <pre class="brush:js;toolbar:false;">@Component({ selector: &amp;#39;counter&amp;#39;, template: `{{count}}`, changeDetection: ChangeDetectionStrategy.OnPush }) export class CounterComponent { count = 0; constructor(private cdr: ChangeDetectorRef) { setTimeout(() =&gt; { this.count = 5; this.cdr.detectChanges(); }, 1000); } }</pre>3 番目は

markForCheck()

で、変更検出はトリガーされません。代わりに、現在または次の変更検出ループで onPush が設定されているすべての祖先タグを検出します。 <pre class="brush:js;toolbar:false;">tick() { try { this._views.forEach((view) =&gt; view.detectChanges()); ... } catch (e) { ... } }</pre>手動での変更検出の実行は「ハック」ではないことに注意してください。これは Angular の意図的な設計であり、非常に合理的な動作です (もちろん、合理的なシナリオの下では)。

Angular Async パイプ

async

パイプは Observable または Promise をサブスクライブし、それが出力する最新の値を返します。

input()

は監視可能な onPush コンポーネントです。 <pre class="brush:js;toolbar:false;">markForCheck(): void { markParentViewsForCheck(this._view); } export function markParentViewsForCheck(view: ViewData) { let currView: ViewData|null = view; while (currView) { if (currView.def.flags &amp; ViewFlags.OnPush) { currView.state |= ViewState.ChecksEnabled; } currView = currView.viewContainerParent || currView.parent; } }</pre><pre class="brush:js;toolbar:false;">@Component({ template: ` &lt;button (click)=&quot;add()&quot;&gt;Add&lt;/button&gt; &lt;app-list [items$]=&quot;items$&quot;&gt;&lt;/app-list&gt; ` }) export class AppComponent { items = []; items$ = new BehaviorSubject(this.items); add() { this.items.push({ title: Math.random() }) this.items$.next(this.items); } }</pre>ボタンをクリックしても、ビューの更新は表示されません。これは、上記の状況がいずれも発生していないため、Angular は現在の変更検出サイクルでコンポーネントをチェックしません。

现在,让我们加上async pipe试试。

@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}

现在可以看到当我们点击按钮时,视图也更新了。原因是当新的值被发射出来时,async pipe将该组件标记为发生了更改需要检查。我们可以在源码中看到:

private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}

Angular为我们调用markForCheck(),所以我们能看到视图更新了即使input的引用没有发生改变。

如果一个组件仅仅依赖于它的input属性,并且input属性是observable,那么这个组件只有在它的input属性发射一个事件的时候才会发生改变。

Quick tip:对外部暴露你的subject是不值得提倡的,总是使用asObservable()方法来暴露该observable。

onPush和视图查询

@Component({
  selector: &#39;app-tabs&#39;,
  template: `<ng-content></ng-content>`
})
export class TabsComponent implements OnInit {
  @ContentChild(TabComponent) tab: TabComponent;

  ngAfterContentInit() {
    setTimeout(() => {
      this.tab.content = &#39;Content&#39;; 
    }, 3000);
  }
}
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
<app-tabs>
  <app-tab></app-tab>
</app-tabs>

也许你会以为3秒后Angular将会使用新的内容更新tab组件。

毕竟,我们更新来onPush组件的input引用,这将会触发变更检测不是吗?

然而,在这种情况下,它并不生效。Angular不知道我们正在更新tab组件的input属性,在模板中定义input()是让Angular知道应在变更检测循环中检查此属性的唯一途径。

例如:

<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>

因为当我们明确的在模板中定义了input(),Angular会创建一个叫updateRenderer()的方法,它会在每个变更检测循环中都对content的值进行追踪。

Angular での onPush 変更検出戦略をすばやく理解する

在这种情况下简单的解决办法使用setter然后调用markForCheck()

@Component({
  selector: &#39;app-tab&#39;,
  template: `
    {{_content}}
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  _content;

  @Input() set content(value) {
    this._content = value;
    this.cdr.markForCheck();
  }

  constructor(private cdr: ChangeDetectorRef) {}

}

=== onPush++

在理解了onPush的强大之后,我们来利用它创造一个更高性能的应用。onPush组件越多,Angular需要执行的检查就越少。让我们看看你一个真是的例子:

我们又一个todos组件,它有一个todos作为input()。

@Component({
  selector: &#39;app-todos&#39;,
  template: `
     <div *ngFor="let todo of todos">
       {{todo.title}} - {{runChangeDetection}}
     </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;

  get runChangeDetection() {
    console.log(&#39;TodosComponent - Checking the view&#39;);
    return true;
  }

}
@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-todos [todos]="todos"></app-todos>
  `
})
export class AppComponent {
  todos = [{ title: &#39;One&#39; }, { title: &#39;Two&#39; }];

  add() {
    this.todos = [...this.todos, { title: &#39;Three&#39; }];
  }
}

上述方法的缺点是,当我们单击添加按钮时,即使之前的数据没有任何更改,Angular也需要检查每个todo。因此第一次单击后,控制台中将显示三个日志。

在上面的示例中,只有一个表达式需要检查,但是想象一下如果是一个有多个绑定(ngIf,ngClass,表达式等)的真实组件,这将会非常耗性能。

我们白白的执行了变更检测!

更高效的方法是创建一个todo组件并将其变更检测策略定义为onPush。例如:

@Component({
  selector: &#39;app-todos&#39;,
  template: `
    <app-todo [todo]="todo" *ngFor="let todo of todos"></app-todo>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodosComponent {
  @Input() todos;
}

@Component({
  selector: &#39;app-todo&#39;,
  template: `{{todo.title}} {{runChangeDetection}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TodoComponent {
  @Input() todo;

  get runChangeDetection() {
    console.log(&#39;TodoComponent - Checking the view&#39;);
    return true;
  }

}

现在,当我们单击添加按钮时,控制台中只会看到一个日志,因为其他的todo组件的input均未更改,因此不会去检查其视图。

并且,通过创建更小粒度的组件,我们的代码变得更具可读性和可重用性。

原文链接: https://netbasal.com/a-comprehensive-guide-to-angular-onpush-change-detection-strategy-5bac493074a4

原文作者:Netanel Basal

译者:淼淼

更多编程相关知识,请访问:编程视频!!

以上がAngular での onPush 変更検出戦略をすばやく理解するの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はjuejin.cnで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。