Maison  >  Article  >  interface Web  >  Un examen plus approfondi de la détection des changements dans Angular

Un examen plus approfondi de la détection des changements dans Angular

青灯夜游
青灯夜游avant
2021-04-09 11:03:221776parcourir

Cet article vous explique la détection des changements dans Angular. Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer. J'espère qu'il sera utile à tout le monde.

Un examen plus approfondi de la détection des changements dans Angular

Lien du texte original : https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f

Cet article vient d'être traduit par l'auteur à la lecture du texte original . Il existe de nombreuses erreurs et expressions incorrectes, la lecture du texte original est donc la meilleure option.

Si vous souhaitez, comme moi, approfondir votre compréhension de la détection de changements d'Angular, accéder au code source est sans aucun doute un choix incontournable, car il y a vraiment trop peu d'informations pertinentes sur Internet. La plupart des articles soulignent simplement que chaque composant possède son propre détecteur de changement, mais ne vont pas plus loin. La plupart d'entre eux se concentrent sur l'utilisation d'objets immuables et de stratégies de détection de changement. Le but de cet article est donc de vous expliquer pourquoi l’utilisation d’objets immuables fonctionne ? Comment la stratégie de détection des changements affecte-t-elle la détection ? Parallèlement, cet article vous permet également de proposer des méthodes d'optimisation des performances correspondantes dans différents scénarios.

Cet article se compose de deux parties. La première partie est plus technique et comporte de nombreuses références de code source. Explique principalement les détails du fonctionnement sous-jacent de la détection des changements. Basé sur Angular-4.0.1. Il est à noter que la stratégie de détection des changements des versions postérieures à la 2.4.1 a subi des changements majeurs. Si vous êtes intéressé par la détection des modifications des versions précédentes, vous pouvez lire cette réponse.

Tutoriels associés recommandés : "Tutoriel angulaire "

La deuxième partie explique principalement comment utiliser la détection de changement dans les applications. Cette partie est la même pour Angular2+. Parce que l'API publique d'Angular n'a pas changé.

Concept de base - Vue


La documentation d'Angular mentionne tout au long qu'une application Angular est une arborescence de composants. Mais la couche inférieure d'Angular utilise en fait une vue d'abstraction de bas niveau. Vues La relation entre les vues et les composants est simple : une vue est associée à un composant et vice versa. Chaque vue conserve une référence à son instance de composant associée dans sa propriété de composant. Toutes les opérations telles que la détection d'attributs et les mises à jour du DOM sont effectuées sur la vue. Par conséquent, il est techniquement plus précis de décrire une application Angular comme une arborescence de vues, puisque les composants sont une description de niveau supérieur des vues. La vue correspondante est décrite dans le code source comme suit :

Une vue est un élément fondamental de l'interface utilisateur de l'application. Il s'agit du plus petit groupe d'éléments qui sont créés et détruits ensemble.

La vue est la plus petite unité qui constitue l'interface de l'application. C'est une combinaison d'une série d'éléments qui sont créés et détruits ensemble.

Les propriétés des éléments dans une vue peuvent changer, mais la structure (nombre et ordre) des éléments dans une vue ne peut pas être modifiée. La structure des éléments ne peut être effectuée qu'en les insérant, en les déplaçant ou en les déplaçant. suppression des vues imbriquées via un ViewContainerRef. Chaque vue peut contenir de nombreux conteneurs de vues.

Les attributs des éléments de la vue peuvent changer, mais le nombre et l'ordre des éléments dans la vue ne peuvent pas changer. Si vous souhaitez le modifier, vous devez effectuer des opérations d'insertion, de déplacement et de suppression via VireContainerRef. Chaque vue comprendra plusieurs conteneurs de vues.

Dans cet article, les notions de composant et de vue composant sont interchangeables.

Il est à noter que de nombreux articles sur Internet utilisent la vue que nous décrivons ici comme un objet de détection de changement ou ChangeDetectorRef. En fait, il n'y a pas d'objet séparé dans Angular pour la détection des changements, toute détection des changements est exécutée directement sur la vue.

export interface ViewData {
  def: ViewDefinition;
  root: RootData;
  renderer: Renderer2;
  // index of component provider / anchor.
  parentNodeDef: NodeDef|null;
  parent: ViewData|null;
  viewContainerParent: ViewData|null;
  component: any;
  context: any;
  // Attention: Never loop over this, as this will
  // create a polymorphic usage site.
  // Instead: Always loop over ViewDefinition.nodes,
  // and call the right accessor (e.g. `elementData`) based on
  // the NodeType.
  nodes: {[key: number]: NodeData};
  state: ViewState;
  oldValues: any[];
  disposables: DisposableFn[]|null;
}

L'état de la vue


Chaque vue a son propre état En fonction des valeurs de ces états, Angular décidera de mettre à jour. cette vue et toutes les autres vues. Exécutez la détection des modifications sur les sous-vues. Les vues ont de nombreuses valeurs d'état, mais pour cet article, les quatre valeurs d'état suivantes sont les plus importantes :

// Bitmask of states
export const enum ViewState {
  FirstCheck = 1 << 0,
  ChecksEnabled = 1 << 1,
  Errored = 1 << 2,
  Destroyed = 1 << 3
}

Si la valeur CheckedEnabled est false ou si la vue est dans Errored ou Destroyed, la détection des modifications pour cette vue ne sera pas effectuée. Par défaut, toutes les vues sont initialisées avec CheckEnabled sauf si ChangeDetectionStrategy.onPush est utilisé. Nous parlerons de onPush plus tard. Ces états peuvent également être combinés. Par exemple, une vue peut avoir à la fois des membres FirstCheck et CheckEnabled.

针对操作视图,Angular中有一些封装出的高级概念,详见这里。一个概念是ViewRef。他的_view属性囊括了组件视图,同时它还有一个方法detectChanges。当一个异步事件触发时,Angular从他的最顶层的ViewRef开始触发变更检测,然后对子视图继续进行变更检测。

ChangeDectionRef可以被注入到组件的构造函数中。这个类的定义如下:

export declare abstract class ChangeDetectorRef {
    abstract checkNoChanges(): void;
    abstract detach(): void;
    abstract detectChanges(): void;
    abstract markForCheck(): void;
    abstract reattach(): void;
}
export abstract class ViewRef extends ChangeDetectorRef {
    /**
     * Destroys the view and all of the data structures associated with it.
     */
    abstract destroy(): void;
    abstract get destroyed(): boolean;
    abstract onDestroy(callback: Function): any
}

变更检测操作


负责对视图运行变更检测的主要逻辑属于checkAndUpdateView方法。他的大部分功能都是对子组件视图进行操作。从宿主组件开始,这个方法被递归调用作用于每一个组件。这意味着当递归树展开时,在下一次调用这个方法时子组件会成为父组件。

当在某个特定视图上开始触发这个方法时,以下操作会依次发生:

  • 如果这是视图的第一次检测,将ViewState.firstCheck设置为true,否则为false;

  • 检查并更新子组件/指令的输入属性-checkAndUpdateDirectiveInline

  • 更新子视图的变更检测状态(属于变更检测策略实现的一部分)

  • 对内嵌视图运行变更检测(重复列表中的步骤)

  • 如果绑定的值发生变化,调用子组件的onChanges生命周期钩子;

  • 调用子组件的OnInit和DoCheck两个生命周期钩子(OnInit只在第一次变更检测时调用)

  • 在子组件视图上更新ContentChildren列表-checkAndUpdateQuery

  • 调用子组件的AfterContentInit和AfterContentChecked(前者只在第一次检测时调用)-callProviderLifecycles

  • 如果当前视图组件上的属性发生变化,更新DOM

  • 对子视图执行变更检测-callViewAction

  • 更新当前视图组件的ViewChildren列表-checkAndUpdateQuery

  • 调用子组件的AfterViewInit和AfterViewChecked-callProviderLifecycles

  • 对当前视图禁用检测

在以上操作中有几点需要注意

深入这些操作的含义


假设我们现在有一棵组件树:

在上面的讲解中我们得知了每个组件都和一个组件视图相关联。每个视图都使用ViewState.checksEnabled初始化了。这意味着当Angular开始变更检测时,整棵组件树上的所有组件都会被检测;

假设此时我们需要禁用AComponent和它的子组件的变更检测,我们只要将它的ViewState.checksEnabled设置为false就行。这听起来很容易,但是改变state的值是一个很底层的操作,因此Angular在视图上提供了很多方法。通过ChangeDetectorRef每个组件可以获得与之关联的视图。

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}

detach

这个方法简单的禁止了对当前视图的检测;

detach(): void {
    this._view.state &= ~ViewState.checksEnabled;
}

在组件中的使用方法:

export class AComponent {
    constructor(
        private cd: ChangeDectectorRef,
    ) {
        this.cd.detach();
    }
}

这样就会导致在接下来的变更检测中AComponent及子组件都会被跳过。

这里有两点需要注意:

  • 虽然我们只修改了AComponent的state值,但是他的子组件也不会被执行变更检测;
  • 由于AComponent及其子组件不会有变更检测,因此他们的DOM也不会有任何更新

下面是一个简单示例,点击按钮后在输入框中修改就再也不会引起下面的p标签的变化,外部父组件传递进来的值发生变化也不会触发变更检测:

import { Component, OnInit, ChangeDetectorRef } from &#39;@angular/core&#39;;
@Component({
    selector: &#39;app-change-dection&#39;,
    template: `
    <input [(ngModel)]="name">
    <button (click)="stopCheck()">停止检测</button>
    <p>{{name}}</p>
    `,
    styleUrls: [&#39;./change-dection.component.css&#39;]
})
export class ChangeDectionComponent implements OnInit {
    name = &#39;erik&#39;;
    constructor(
        private cd: ChangeDetectorRef,
    ) { }
    ngOnInit() {
    }
    stopCheck() {
        this.cd.detach();
    }
}

reattach

文章第一部分提到:如果AComponent的输入属性aProp发生变化,OnChanges生命周期钩子仍会被调用,这意味着一旦我们得知输入属性发生变化,我们可以激活当前组件的变更检测并在下一个tick中继续detach变更检测。

reattach(): void { 
    this._view.state |= ViewState.ChecksEnabled; 
}
export class ChangeDectionComponent implements OnInit, OnChanges {
    @Input() aProp: string;
    name = &#39;erik&#39;;
    constructor(
        private cd: ChangeDetectorRef,
    ) { }
    ngOnInit() {
    }
    ngOnChanges(change) {
        this.cd.reattach();
        setTimeout(() => {
            this.cd.detach();
        });
    }
}

上面这种做法几乎与将ChangeDetectionStrategy改为OnPush是等效的。他们都在第一轮变更检测后禁用了检测,当父组件向子组件传值发生变化时激活变更检测,然后又禁用变更检测。

需要注意的是,在这种情况下,只有被禁用检测分支最顶层组件的OnChanges钩子才会被触发,并不是这个分支的所有组件的OnChanges都会被触发,原因也很简单,被禁用检测的这个分支内不存在了变更检测,自然内部也不会向子元素变更所传递的值,但是顶层的元素仍可以接受到外部变更的输入属性。

译注:其实将retach()和detach()放在ngOnChanges()和OnPush策略还是不一样的,OnPush策略的确是只有在input值的引用发生变化时才出发变更检测,这一点是正确的,但是OnPush策略本身并不影响组件内部的值的变化引起的变更检测,而上例中组件内部的变更检测也会被禁用。如果将这段逻辑放在ngDoCheck()中才更正确一点。

maskForCheck

上面的reattach()方法可以对当前组件开启变更检测,然而如果这个组件的父组件或者更上层的组件的变更检测仍被禁用,用reattach()后是没有任何作用的。这意味着reattach()方法只对被禁用检测分支的最顶层组件有意义。

因此我们需要一个方法,可以将当前元素及所有祖先元素直到根元素的变更检测都开启。ChangeDetectorRef提供了markForCheck方法:

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

在这个实现中,它简单的向上迭代并启用对所有直到根组件的祖先组件的检查。

这个方法在什么时候有用呢?禁用变更检测策略之后,ngDoCheck生命周期还是会像ngOnChanges一样被触发。当然,跟OnChanges一样,DoCheck也只会在禁用检测分支的顶部组件上被调用。但是我们就可以利用这个生命周期钩子来实现自己的业务逻辑和将这个组件标记为可以进行一轮变更检测。

由于Angular只检测对象引用,我们需要通过对对象的某些属性来进行这种脏检查:

// 这里如果外部items变化为改变引用位置,此组件是不会执行变更检测的
// 但是如果在DoCheck()钩子中调用markForCheck
// 由于OnPush策略不影响DoCheck的执行,这样就可以侦测到这个变更
Component({
   ...,
   changeDetection: ChangeDetectionStrategy.OnPush
})
MyComponent {
    @Input() items;
    prevLength;
    constructor(cd: ChangeDetectorRef) {}

    ngOnInit() {
        this.prevLength = this.items.length;
    }

    ngDoCheck() {
        // 通过比较前后的数组长度
        if (this.items.length !== this.prevLength) {
            this.cd.markForCheck(); 
            this.prevLenght = this.items.length;
        }
    }
}

detectChanges

Angular提供了一个方法detectChanges,对当前组件和所有子组件运行一轮变更检测。这个方法会无视组件的ViewState,也就是说这个方法不会改变组件的变更检测策略,组件仍会维持原有的会被检测或不会被检测状态。

export class AComponent {
  @Input() inputAProp;

  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }

  ngOnChanges(values) {
    this.cd.detectChanges();
  }
}

通过这个方法我们可以实现一个类似Angular.js的手动调用脏检查。

checkNoChanges

这个方法是用来当前变更检测没有产生任何变化。他执行了文章第一部分1,7,8三个操作,并在发现有变更导致DOM需要更新时抛出异常。

结束!哈!

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

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