Heim  >  Artikel  >  Web-Frontend  >  Verstehen Sie schnell die onPush-Änderungserkennungsstrategie in Angular

Verstehen Sie schnell die onPush-Änderungserkennungsstrategie in Angular

青灯夜游
青灯夜游nach vorne
2021-09-09 19:44:362232Durchsuche

Dieser Artikel vermittelt Ihnen ein tiefgreifendes Verständnis der onPush-Änderungserkennungsstrategie in Angular. Ich hoffe, er wird Ihnen hilfreich sein!

Verstehen Sie schnell die onPush-Änderungserkennungsstrategie in Angular

Standardstrategie zur Änderungserkennung

Standardmäßig verwendet Angular die Strategie ChangeDetectionStrategy.Default zur Änderungserkennung. ChangeDetectionStrategy.Default策略来进行变更检测。

默认策略并不事先对应用做出任何假设,因此,每当用户事件、记时器、XHR、promise等事件使应用中的数据将发生了改变时,所有的组件中都会执行变更检测。

这意味着从点击事件到从ajax调用接收到的数据之类的任何事件都会触发更改检测。

通过在组件中定义一个getter并且在模板中使用它,我们可以很容易的看出这一点:

@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将会执行一遍变更检测循环,在console里我们可以看到两行“Checking the view”的日志。

这种技术被称作脏检查。为了知道视图是否需要更新,Angular需要访问新值并和旧值比较来判断是否需要更新视图。

现在想象一下,如果有一个有成千上万个表达式的大应用,Angular去检查每一个表达式,我们可能会遇到性能上的问题。

那么有没有办法让我们主动告诉Angular什么时候去检查我们的组件呢?

OnPush变更检测策略

我们可以将组件的ChangeDetectionStrategy设置成ChangeDetectionStrategy.OnPush

这将告诉Angular该组件仅仅依赖于它的@inputs(),只有在以下几种情况才需要检查:

1. Input引用发生改变

通过设置onPush变更检测测策略,我们与Angular约定强制使用不可变对象(或稍后将要介绍的observables)。

在变更检测的上下文中使用不可变对象的好处是,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将旧值和新值的引用进行比较,类似于:

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

值得一提的是numbers, booleans, strings, null 、undefined都是原始类型。所有的原始类型都是按值传递的. Objects, arrays, 还有 functions 也是按值传递的,只不过值是引用地址的副本

所以为了触发对该组件的变更检测,我们需要更改这个object的引用。

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

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

将对象引用改变后,我们将看到视图已被检查,新值被展示出来。

2.源于该组件或其子组件的事件

当在一个组件或者其子组件中触发了某一个事件时,这个组件的内部状态会更新。 例如:

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

  add() {
    this.count++;
  }

}

当我们点击按钮时,Angular执行变更检测循环并更新视图。

你可能会想,按照我们开头讲述的那样,每一次异步的API都会触发变更检测,但是并不是这样。

你会发现这个规则只适用于DOM事件,下面这些API并不会触发变更检测:

@Component({
  template: `...`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
  count = 0;

  constructor() {
    setTimeout(() => this.count = 5, 0);

    setInterval(() => this.count = 5, 100);

    Promise.resolve().then(() => this.count = 5); 
    
    this.http.get(&#39;https://count.com&#39;).subscribe(res => {
      this.count = res;
    });
  }

  add() {
    this.count++;
  }

注意你仍然是更新了该属性的,所以在下一个变更检测流程中,比如去点击按钮,count值将会变成6(5+1)。

3. 显示的去执行变更检测

Angular给我们提供了3种方法来触发变更检测。

第一个是detectChanges()来告诉Angular在该组件和它的子组件中去执行变更检测。

@Component({
  selector: &#39;counter&#39;,
  template: `{{count}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent { 
  count = 0;

  constructor(private cdr: ChangeDetectorRef) {

    setTimeout(() => {
      this.count = 5;
      this.cdr.detectChanges();
    }, 1000);

  }

}

第二个是ApplicationRef.tick(),它告诉Angular来对整个应用程序执行变更检测。

tick() {
 
  try {
    this._views.forEach((view) => view.detectChanges());
    ...
  } catch (e) {
    ...
  }
}

第三是markForCheck(),它不会触发变更检测。相反,它会将所有设置了onPush的祖先标记,在当前或者下一次变更检测循环中检测。

markForCheck(): void { 
  markParentViewsForCheck(this._view); 
}

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}

需要注意的是,手动执行变更检测并不是一种“hack”,这是Angular有意的设计并且是非常合理的行为(当然,是在合理的场景下)。

Angular Async  pipe

async pipe会订阅一个 Observable 或 Promise,并返回它发出的最近一个值。

让我们看一个input()

Die Standardstrategie trifft im Voraus keine Annahmen über die Anwendung. Daher wird eine Änderungserkennung in allen Komponenten durchgeführt, wenn Benutzerereignisse, Timer, XHR, Versprechen und andere Ereignisse dazu führen, dass sich die Daten in der Anwendung ändern.

Das bedeutet, dass jedes Ereignis, von einem Klickereignis bis hin zu Daten, die von einem Ajax-Aufruf empfangen werden, eine Änderungserkennung auslöst. 🎜🎜Wir können dies leicht erkennen, indem wir einen Getter in der Komponente definieren und ihn in der Vorlage verwenden: 🎜
@Component({
  template: `
    <button (click)="add()">Add</button>
    <app-list [items$]="items$"></app-list>
  `
})
export class AppComponent {
  items = [];
  items$ = new BehaviorSubject(this.items);

  add() {
    this.items.push({ title: Math.random() })
    this.items$.next(this.items);
  }
}
@Component({
  template: `
     <div *ngFor="let item of _items ; ">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items: Observable<Item>;
  _items: Item[];
  
  ngOnInit() {
    this.items.subscribe(items => {
      this._items = items;
    });
  }

}
🎜Nach der Ausführung des obigen Codes, wann immer wir auf die Schaltfläche klicken. Angular führt eine Änderungserkennungsschleife aus und in der Konsole sehen wir zwei Zeilen mit „Überprüfung der Ansicht“-Protokollen. 🎜🎜Diese Technik wird Dirty Checking genannt. Um zu wissen, ob die Ansicht aktualisiert werden muss, muss Angular auf den neuen Wert zugreifen und ihn mit dem alten Wert vergleichen, um festzustellen, ob die Ansicht aktualisiert werden muss. 🎜🎜Stellen Sie sich nun vor, wenn es eine große Anwendung mit Tausenden von Ausdrücken gibt und Angular jeden Ausdruck überprüft, könnten Leistungsprobleme auftreten. 🎜🎜Gibt es also eine Möglichkeit für uns, Angular proaktiv mitzuteilen, wann wir unsere Komponenten überprüfen sollen? 🎜

OnPush-Änderungserkennungsstrategie🎜🎜Wir können die ChangeDetectionStrategy der Komponente auf ChangeDetectionStrategy.OnPush setzen. 🎜🎜Dadurch wird Angular mitgeteilt, dass die Komponente nur von ihrem @inputs() abhängt, was nur in den folgenden Situationen überprüft werden muss: 🎜

1 . EingabeReferenzänderungen

🎜Durch die Festlegung der Änderungserkennungsstrategie onPush stimmen wir mit Angular überein, die Verwendung unveränderlicher Objekte (oder Observables, die eingeführt werden) zu erzwingen später). 🎜🎜Der Vorteil der Verwendung unveränderlicher Objekte im Kontext der Änderungserkennung besteht darin, dass Angular feststellen kann, ob die Ansicht überprüft werden muss, indem überprüft wird, ob sich die Referenz geändert hat. Dies wird viel einfacher sein als eine eingehende Inspektion. 🎜🎜Versuchen wir, ein Objekt zu ändern und sehen wir uns das Ergebnis an. 🎜
@Component({
  template: `
    <div *ngFor="let item of items | async">{{item.title}}</div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class ListComponent implements OnInit {
  @Input() items;
}
private _updateLatestValue(async: any, value: Object): void {
  if (async === this._obj) {
    this._latestValue = value;
    this._ref.markForCheck();
  }
}
🎜Sie können derzeit keine Protokolle sehen, wenn Sie auf die Schaltfläche klicken. Dies liegt daran, dass Angular die Referenzen des alten Werts und des neuen Werts vergleicht, ähnlich wie: 🎜
@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);
  }
}
🎜Es ist erwähnenswert, dass Zahlen, Boolesche Werte, strings, null, undefiniert sind allesamt primitive Typen. Alle primitiven Typen werden als Wert übergeben. Objekte, Arrays und Funktionen werden ebenfalls als Wert übergeben, aber der Wert istKopie der Referenzadresse. 🎜🎜Um also die Änderungserkennung für diese Komponente auszulösen, müssen wir den Verweis auf dieses Objekt ändern. 🎜
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
🎜Nach dem Ändern der Objektreferenz sehen wir, dass die Ansicht überprüft wurde und der neue Wert angezeigt wird. 🎜

2. Ereignisse, die von dieser Komponente oder ihren Unterkomponenten ausgehen

🎜Wenn ein Ereignis in einer Komponente oder ihren Unterkomponenten ausgelöst wird, wird der interne Status aktualisiert. Zum Beispiel: 🎜
<app-tabs>
  <app-tab></app-tab>
</app-tabs>
🎜 Wenn wir auf die Schaltfläche klicken, führt Angular eine Änderungserkennungsschleife aus und aktualisiert die Ansicht. 🎜🎜Sie denken vielleicht, dass, wie wir eingangs beschrieben haben, jede asynchrone API eine Änderungserkennung auslöst, aber das ist nicht der Fall. 🎜🎜Sie werden feststellen, dass diese Regel nur für DOM-Ereignisse gilt. Die folgenden APIs lösen keine Änderungserkennung aus: 🎜
<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>
🎜Beachten Sie, dass Sie das Attribut noch aktualisiert haben. Zählen Sie daher beim nächsten Änderungserkennungsprozess, z. B. beim Klicken auf eine Schaltfläche Der Wert wird 6(5+1). 🎜

3. Anzeigeänderungserkennung

🎜Angular stellt uns 3 Methoden zur Verfügung, um die Änderungserkennung auszulösen. 🎜🎜Der erste ist detectChanges(), um Angular anzuweisen, eine Änderungserkennung in dieser Komponente und ihren Unterkomponenten durchzuführen. 🎜
@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) {}

}
🎜Der zweite ist ApplicationRef.tick(), der Angular anweist, eine Änderungserkennung für die gesamte Anwendung durchzuführen. 🎜
@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;
  }

}
🎜Der dritte ist markForCheck(), der keine Änderungserkennung auslöst. Stattdessen werden alle Vorfahren-Tags erkannt, bei denen onPush im aktuellen oder nächsten Änderungserkennungszyklus festgelegt ist. 🎜
@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; }];
  }
}
🎜Es sollte beachtet werden, dass die manuelle Durchführung der Änderungserkennung kein „Hack“ ist, sondern Angulars absichtliches Design und ein sehr vernünftiges Verhalten (natürlich unter vernünftigen Szenarien). 🎜

Angular Async-Pipe

🎜async Die Pipe abonniert ein Observable oder Promise und gibt den neuesten Wert zurück, den sie ausgibt. 🎜🎜Schauen wir uns eine onPush-Komponente an, bei der input() beobachtbar ist. 🎜
@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;
  }

}
rrreee🎜Wenn wir auf die Schaltfläche klicken, können wir die Ansichtsaktualisierung nicht sehen. Dies liegt daran, dass keine der oben genannten Situationen aufgetreten ist, sodass Angular die Komponente im aktuellen Änderungserkennungszyklus nicht überprüft. 🎜

现在,让我们加上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的值进行追踪。

Verstehen Sie schnell die onPush-Änderungserkennungsstrategie in Angular

在这种情况下简单的解决办法使用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

译者:淼淼

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

Das obige ist der detaillierte Inhalt vonVerstehen Sie schnell die onPush-Änderungserkennungsstrategie in Angular. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen