web-dev-qa-db-fra.com

détection de changement angulaire 2 et ChangeDetectionStrategy.OnPush

J'essaie de comprendre le mécanisme ChangeDetectionStrategy.OnPush

D'après mes lectures, une détection de changement fonctionne en comparant l'ancienne valeur à la nouvelle valeur. Cette comparaison retournera false si la référence de l'objet n'a pas changé.

Cependant, il semble exister certains scénarios dans lesquels cette "règle" est contournée. Pourriez-vous expliquer comment cela fonctionne? 

23
Ced

D'accord, puisque cela m'a pris toute une soirée pour comprendre, j'ai fait un CV pour tout régler dans ma tête et cela pourrait aider les futurs lecteurs. Commençons donc par clarifier certaines choses:

Les changements viennent d'événements

Un composant peut avoir des champs. Ces champs ne changent qu’après une sorte d’événement et seulement après. 

Nous pouvons définir un événement comme un clic de souris, une requête ajax, setTimeout ...

Les données circulent de haut en bas

Le flux de données angulaire est une rue à sens unique. Cela signifie que les données ne circulent pas des enfants aux parents. Seulement du parent aux enfants, par exemple via la balise @Input. La seule façon de rendre un composant supérieur conscient de certains changements chez un enfant consiste à utiliser event. Ce qui nous amène à:

Détection de changement de déclencheur d'événement

Lorsqu'un événement se produit, le cadre angulaire vérifie chaque composant de haut en bas pour voir s'il a été modifié. Si l'un d'entre eux a changé, la vue est mise à jour en conséquence.

Angular vérifie tous les composants après le déclenchement d'un événement. Supposons que vous ayez un événement click sur un composant qui est le composant au niveau le plus bas, ce qui signifie qu'il a des parents mais pas d'enfants. Ce clic pourrait déclencher une modification d'un composant parent via un émetteur d'événement, un service, etc. Angular ne sait pas si les parents vont changer ou non. C'est pourquoi Angular vérifie tous les composants après le déclenchement d'un événement par défaut.

Pour voir s’ils ont changé d’angle, utilisez la classe ChangeDetector.

Détecteur de changement

Chaque composant est associé à une classe de détecteurs de changement. Il est utilisé pour vérifier si un composant a changé d'état après un événement et pour voir si la vue doit être mise à jour. Lorsqu'un événement survient (clic de souris, etc.), ce processus de détection des modifications se produit pour tous les composants - par défaut -.

Par exemple si nous avons un ParentComponent:

@Component({
  selector: 'comp-parent',
  template:'<comp-child [name]="name"></comp-child>'
})
class ParentComponent{
  name:string;
} 

Nous aurons un détecteur de changement attaché à la ParentComponent qui ressemble à ceci:

class ParentComponentChangeDetector{
    oldName:string; // saves the old state of the component.

    isChanged(newName){
      if(this.oldName !== newName)
          return true;
      else
          return false;
    }
}

Changer les propriétés de l'objet

Comme vous l'avez peut-être remarqué, la méthode isChanged retournera false si vous modifiez une propriété d'objet. Effectivement

let prop = {name:"cat"};
let oldProp = prop;
//change prop
prop.name = "dog";
oldProp === prop; //true

Depuis quand une propriété d'objet peut changer sans renvoyer true dans la changeDetectorisChanged(), angular supposera que chaque composant ci-dessous peut également avoir changé. Donc, il va simplement vérifier la détection de changement dans tous les composants.

Exemple: nous avons ici un composant avec un sous-composant. Bien que la détection de modification renvoie false pour le composant parent, la vue de l'enfant devrait très bien être mise à jour.

@Component({
  selector: 'parent-comp',
  template: `
    <div class="orange" (click)="person.name='frank'">
      <sub-comp [person]="person"></sub-comp>
    </div>
  `
})
export class ParentComponent {
  person:Person = { name: "thierry" };     
}

// sub component
@Component({
  selector: 'sub-comp',
  template: `
    <div>
      {{person.name}}
    </div>
})
export class SubComponent{
  @Input("person") 
  person:Person;
}

C'est pourquoi le comportement par défaut consiste à vérifier tous les composants. Parce que même si un sous-composant ne peut pas changer si son entrée n'a pas changé, angular ne sait pas avec certitude que son entrée n'a pas vraiment changé. L'objet qui lui est transmis peut être le même mais il peut avoir des propriétés différentes.

Stratégie OnPush

Lorsqu'un composant est marqué avec changeDetection: ChangeDetectionStrategy.OnPush, angular supposera que l'objet en entrée n'a pas changé si la référence à l'objet n'a pas changé. Cela signifie que modifier une propriété ne déclenchera pas la détection de changement. Ainsi, la vue sera désynchronisée avec le modèle.

Exemple

Cet exemple est cool car il montre cela en action. Si vous cliquez sur un composant parent, les propriétés du nom d'objet en entrée sont modifiées . Si vous cochez la méthode click() dans le composant parent, vous remarquerez qu'il génère la propriété du composant enfant dans la console. Cette propriété a changé..Mais vous ne pouvez pas le voir visuellement. C'est parce que la vue n'a pas été mise à jour. En raison de la stratégie OnPush, le processus de détection de changement n'a pas eu lieu car l'objet de référence n'a pas changé.

Plnkr

@Component({
  selector: 'my-app',
  template: `
    <div class="orange" (click)="click()">
      <sub-comp [person]="person" #sub></sub-comp>
    </div>
  `
})
export class App {
  person:Person = { name: "thierry" };
  @ViewChild("sub") sub;

  click(){
    this.person.name = "Jean";
    console.log(this.sub.person);
  }
}

// sub component
@Component({
  selector: 'sub-comp',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div>
      {{person.name}}
    </div>
  `
})
export class SubComponent{
  @Input("person") 
  person:Person;
}

export interface Person{
  name:string,
}

Après le clic, le nom est toujours thierry dans la vue mais pas dans le composant lui-même


Un événement déclenché à l'intérieur d'un composant déclenchera la détection des modifications.

Nous arrivons ici à ce qui m'a confondu dans ma question initiale. Le composant ci-dessous est marqué avec la stratégie OnPush, mais la vue est mise à jour quand elle change.

Plnkr

@Component({
  selector: 'my-app',
  template: `
    <div class="orange" >
      <sub-comp ></sub-comp>
    </div>
  `,
  styles:[`
    .orange{ background:orange; width:250px; height:250px;}
  `]
})
export class App {
  person:Person = { name: "thierry" };      
  click(){
    this.person.name = "Jean";
    console.log(this.sub.person);
  }

}

// sub component
@Component({
  selector: 'sub-comp',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="grey" (click)="click()">
      {{person.name}}
    </div>
  `,
  styles:[`
    .grey{ background:#ccc; width:100px; height:100px;}
  `]
})
export class SubComponent{
  @Input()
  person:Person = { name:"jhon" };
  click(){
    this.person.name = "mich";
  }
}

Nous voyons donc ici que l'entrée d'objet n'a pas changé de référence et nous utilisons la stratégie OnPush. Ce qui pourrait nous amener à croire qu'il ne sera pas mis à jour. En fait, il est mis à jour.

Comme Gunter l'a dit dans sa réponse, cela est dû au fait que, avec la stratégie OnPush, la détection de changement se produit pour un composant si:

  • un événement lié est reçu (clic) sur le composant lui-même.
  • un @Input () a été mis à jour (comme dans le ref obj changé)
  • | pipe async a reçu un événement
  • la détection de changement a été appelée "manuellement"

peu importe la stratégie.

Liens

74
Ced

*ngFor fait sa propre détection de changement. Chaque fois que la détection de changement est exécutée, NgFor obtient sa méthode ngDoCheck() et NgFor vérifie si le contenu du tableau a changé.

Dans votre cas, il n'y a pas de changement, car le constructeur est exécuté avant qu'Angular ne commence à rendre la vue.
Si vous souhaitez par exemple ajouter un bouton comme 

<button (click)="persons.Push({name: 'dynamically added', id: persons.length})">add</button>

alors un clic entraînerait un changement que ngFor doit reconnaître.

Avec ChangeDetectionStrategy.OnPush, la détection de changement dans votre composant serait exécutée car avec OnPush, la détection de changement est exécutée lorsque

  • un événement lié est reçu (click)
  • @Input() a été mis à jour par la détection de changement
  • | async canal a reçu un événement
  • la détection de changement a été appelée "manuellement"
17
Günter Zöchbauer

Pour empêcher Application.tick, essayez de détacher changeDetector:

constructor(private cd: ChangeDetectorRef) {

ngAfterViewInit() {
  this.cd.detach();
}

Plunker

7
yurzui

En mode angulaire, nous utilisons fortement la structure parent-enfant. Là nous passons le formulaire de données parent à l’enfant en utilisant @Inputs

Là, si un changement survient sur un ancêtre de l'enfant, la détection du changement se fera dans la fiche arborescente de cet ancêtre.

Mais dans la plupart des situations, nous devrons mettre à jour la vue de l'enfant (détection de changement d'appel) uniquement lorsque ses entrées changent. Pour ce faire, nous pouvons utiliser OnPush _ {ChangeDetectionStrategy et changer les entrées (en utilisant immutables) selon les besoins. LIEN

0
Malindu Sandaruwan