Maison  >  Article  >  interface Web  >  Comprendre rapidement la stratégie de détection de changement onPush dans Angular

Comprendre rapidement la stratégie de détection de changement onPush dans Angular

青灯夜游
青灯夜游avant
2021-09-09 19:44:362243parcourir

Cet article vous donnera une compréhension approfondie de la stratégie de détection des changements onPush dans Angular. J'espère qu'il vous sera utile !

Comprendre rapidement la stratégie de détection de changement onPush dans Angular

Stratégie de détection des changements par défaut

Par défaut, Angular utilise la stratégie ChangeDetectionStrategy.Default pour la détection des changements. 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()

La stratégie par défaut ne fait aucune hypothèse à l'avance sur l'application. Par conséquent, chaque fois que des événements utilisateur, des minuteries, XHR, des promesses et d'autres événements entraînent une modification des données de l'application, la détection des modifications sera effectuée dans tous les composants.

Cela signifie que tout événement, depuis un événement de clic jusqu'aux données reçues d'un appel ajax, déclenchera la détection des modifications. 🎜🎜Nous pouvons facilement le voir en définissant un getter dans le composant et en l'utilisant dans le modèle : 🎜
@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;
    });
  }

}
🎜Après avoir exécuté le code ci-dessus, chaque fois que nous cliquons sur le bouton. Angular exécutera une boucle de détection de changement, et dans la console, nous pouvons voir deux lignes de journaux "Vérification de la vue". 🎜🎜Cette technique est appelée vérification sale. Afin de savoir si la vue doit être mise à jour, Angular doit accéder à la nouvelle valeur et la comparer à l'ancienne valeur pour déterminer si la vue doit être mise à jour. 🎜🎜Imaginez maintenant s'il existe une grande application avec des milliers d'expressions et qu'Angular vérifie chaque expression, nous pourrions rencontrer des problèmes de performances. 🎜🎜Alors, existe-t-il un moyen pour nous d'indiquer de manière proactive à Angular quand vérifier nos composants ? 🎜

Stratégie de détection des changements OnPush🎜🎜Nous pouvons définir le ChangeDetectionStrategy du composant sur ChangeDetectionStrategy.OnPush. 🎜🎜Cela indiquera à Angular que le composant ne dépend que de son @inputs(), qui ne doit être vérifié que dans les situations suivantes : 🎜

1 🎜En définissant la stratégie de détection de changement onPush, nous sommes d'accord avec Angular pour forcer l'utilisation d'objets immuables (ou d'observables qui seront introduits). plus tard). 🎜🎜L'avantage d'utiliser des objets immuables dans le contexte de la détection de changements est qu'Angular peut déterminer si la vue doit être vérifiée en vérifiant si la référence a changé. Ce sera beaucoup plus facile qu’une inspection approfondie. 🎜🎜Essayons de modifier un objet et voyons le résultat. 🎜
@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();
  }
}
🎜Vous ne pouvez voir aucun journal lorsque vous cliquez sur le bouton pour le moment. En effet, Angular compare les références de l'ancienne valeur et de la nouvelle valeur, comme : 🎜
@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);
  }
}
🎜Il convient de mentionner que les nombres, les booléens, les chaînes, null, non définies sont tous des types primitifs. Tous les types primitifs sont transmis par valeur. Les objets, tableaux et fonctions sont également transmis par valeur, mais la valeur estCopie de l'adresse de référence. 🎜🎜Donc afin de déclencher la détection de changement sur ce composant, nous devons changer la référence à cet objet. 🎜
@Component({
  selector: &#39;app-tab&#39;,
  template: `{{content}}`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TabComponent {
  @Input() content;
}
🎜Après avoir changé la référence de l'objet, nous verrons que la vue a été vérifiée et la nouvelle valeur est affichée. 🎜

2. Événements provenant de ce composant ou de ses sous-composants

🎜Lorsqu'un événement est déclenché dans un composant ou ses sous-composants, le statut interne sera mis à jour. Par exemple : 🎜
<app-tabs>
  <app-tab></app-tab>
</app-tabs>
🎜 Lorsque l'on clique sur le bouton, Angular exécute une boucle de détection de changement et met à jour la vue. 🎜🎜Vous pensez peut-être que, comme nous l'avons décrit au début, chaque API asynchrone déclenchera une détection de changement, mais ce n'est pas le cas. 🎜🎜Vous constaterez que cette règle s'applique uniquement aux événements DOM. Les API suivantes ne déclencheront pas la détection de changement : 🎜
<app-tabs>
  <app-tab [content]="content"></app-tab>
</app-tabs>
🎜Notez que vous avez quand même mis à jour l'attribut, donc lors du prochain processus de détection de changement, comme cliquer sur un bouton, comptez Le la valeur deviendra 6(5+1). 🎜

3. Effectuer explicitement la détection des changements

🎜Angular nous fournit 3 méthodes pour déclencher la détection des changements. 🎜🎜Le premier est detectChanges() pour indiquer à Angular d'effectuer une détection des modifications dans ce composant et ses sous-composants. 🎜
@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) {}

}
🎜Le second est ApplicationRef.tick(), qui indique à Angular d'effectuer une détection des modifications sur l'ensemble de l'application. 🎜
@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;
  }

}
🎜Le troisième est markForCheck(), qui ne déclenche pas la détection des changements. Au lieu de cela, il détectera toutes les balises ancêtres avec onPush défini dans la boucle de détection de modification actuelle ou suivante. 🎜
@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; }];
  }
}
🎜Il convient de noter que la détection manuelle des changements n'est pas un "hack", c'est la conception intentionnelle d'Angular et constitue un comportement très raisonnable (bien sûr, dans des scénarios raisonnables). 🎜

Pipe asynchrone angulaire

🎜async Le tube s'abonnera à un observable ou à une promesse et renverra la dernière valeur qu'il émet. 🎜🎜Regardons un composant onPush où input() est observable. 🎜
@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🎜Lorsque nous cliquons sur le bouton, nous ne pouvons pas voir la mise à jour de la vue. En effet, aucune des situations mentionnées ci-dessus ne s'est produite, donc Angular ne vérifiera pas le composant dans le cycle de détection de changement en cours. 🎜

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

Comprendre rapidement la stratégie de détection de changement onPush dans 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

译者:淼淼

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer