angular 2+ 变化检测系列二,检测策略

  我们将创建一个简单的MovieApp来显示有关一部电影的信息。这个应用程序将只包含两个组件:显示有关电影的信息的MovieComponent和包含执行某些操作按钮的电影引用的AppComponent。 我们的AppComponent将有三个属性:slogan,title和actor。最后两个属性将传递给模板中引用的MovieComponent元素。

// app/app.component.ts
import {Component} from '@angular/core'; import {MovieComponent} from './movie.component'; import {Actor} from './actor.model'; @Component({ selector: 'app-root', template: ` <h1>MovieApp</h1> <p>{{ slogan }}</p> <button type="button" (click)="changeActorProperties()"> Change Actor Properties </button> <button type="button" (click)="changeActorObject()"> Change Actor Object </button> <app-movie [title]="title" [actor]="actor"></app-movie>` }) export class AppComponent { slogan = 'Just movie information'; title = 'Terminator 1'; actor = new Actor('Arnold', 'Schwarzenegger'); changeActorProperties() { this.actor.firstName = 'Nicholas'; this.actor.lastName = 'Cage'; } changeActorObject() { this.actor = new Actor('Bruce', 'Willis'); } }

  在上面的代码片段中,我们可以看到我们的组件定义了两个触发不同方法的按钮。 changeActorProperties将通过直接更改actor对象的属性来更新影片的主角。相反,方法changeActorObject将通过创建一个全新的Actor类实例来更改actor的信息。Actor模型非常简单,它只是一个定义actor的firstName和lastName的类。

// app/actor.model.ts
export class Actor { constructor( public firstName: string, public lastName: string) {} }

最后,MovieComponent显示AppComponent在其模板中提供的信息

// app/movie.component.ts
import { Component, Input } from '@angular/core'; import { Actor } from './actor.model'; @Component({ selector: 'app-movie', template: ` <div> <h3>{{ title }}</h3> <p> <label>Actor:</label> <span>{{actor.firstName}} {{actor.lastName}}</span> </p> </div>` }) export class MovieComponent { @Input() title: string; @Input() actor: Actor; }

  在应用程序运行时,Angular将创建称为更改检测器的特殊类,每个组件对应于我们定义的每个组件。在这种情况下,Angular将创建两个类:AppComponent和AppComponent_ChangeDetector.变更检测器的目标是知道自上次更改检测过程运行以来,组件模板中使用的模型属性已更改.为了解这一点,Angular创建了一个适当的变更检测器类的实例,以及一个它应该检查的组件的链接。

  在我们的示例中,因为我们只有AppComponent和MovieComponent的一个实例,所以我们只有一个AppComponent_ChangeDetector实例和MovieComponent_ChangeDetector实例。下面的代码片段是App​​Component_ChangeDetector类的外观概念模型。

class AppComponent_ChangeDetector {

  constructor(
    public previousSlogan: string,
    public previousTitle: string,
    public previousActor: Actor,
    public movieComponent: MovieComponent
  ) {}

  detectChanges(slogan: string, title: string, actor: Actor) {
    if (slogan !== this.previousSlogan) {
      this.previousSlogan = slogan;
      this.movieComponent.slogan = slogan;
    }
    if (title !== this.previousTitle) {
      this.previousTitle = title;
      this.movieComponent.title = title;
    }
    if (actor !== this.previousActor) {
      this.previousActor = actor;
      this.movieComponent.actor = actor;
    }
  }
}

  因为在我们AppComponent的模板中我们引用了三个变量(slogan,title和actor),我们的变换检测器将有三个属性来存储这三个属性的“旧”值,以及对它应该”的AppComponent实例的引用。当更改检测过程想要知道我们的AppComponent实例是否已更改时,它将运行方法detectChanges传递当前模型值以与旧模型值进行比较。如果检测到更改,则组件会更新。

  译者注:这只是变更检测器类如何工作的概念性概述;实际的实施可能会有所不同。

变化检测策略一: Default

  默认情况下,Angular为应用程序中的每个组件定义了一个特定的更改检测策略。为了使这个定义明确,我们可以使用@Component装饰器的属性changeDetection。如下代码

// app/movie.component.ts
import { ChangeDetectionStrategy } from '@angular/core'; @Component({ // ... changeDetection: ChangeDetectionStrategy.Default // 如果不写,缺省值为Default }) export class MovieComponent { // ... }

  让我们看看当用户在使用Defalut策略时单击“更改Actor属性”按钮时会发生什么。更改由事件触发,更改的传播分两个阶段完成:应用阶段和变更检测阶段.

  1. 阶段一(应用阶段) 在第一阶段,应用程序(我们的代码)负责更新模型以响应某些事件。在此方案中,属性actor.firstName和actor.lastName已更新。
  2. 阶段一(变更检测阶段) 现在我们的模型已更新,Angular必须使用更改检测更新模板。变更检测始终从根组件开始,在本例中为AppComponent,并检查绑定到其模板的任何模型属性是否已更改,将每个属性的旧值(在事件触发之前)与新属性(之后)进行比较模型已更新)。 AppComponent模板引用了三个属性,slogan,title和actor,因此其相应的变化检测器进行的比较如下所示:
Is slogan !== previousSlogan? No, it's the same.
Is title !== previousTitle? No, it's the same.
Is actor !== previousActor? No, it's the same.

  请注意,即使我们更改了actor对象的属性,我们也始终使用相同的实例.因为我们正在进行浅层比较,所以即使其内部属性值确实发生变化,询问actor!== previousActor的结果也总是为false。即使更改检测器无法找到任何更改,更改检测的默认策略是遍历树的所有组件,即使它们似乎没有被修改(我们可以手动优化).

  接下来,更改检测在组件层次结构中向下移动,并检查绑定到MovieComponent模板的属性,执行类似的比较:

Is title !== previousTitle? No, it's the same.
Is actorFirstName !== previousActorFirstName? Yes, it has changed.
Is actorLastName !== previousActorLastName? Yes, it has changed.

  最后,Angular检测到绑定到模板的某些属性已更改,因此它将更新DOM以使视图与模型同步.  

变化检测策略二: Onpush

  为了通知Angular我们将遵守之前提到的条件以提高性能,我们将在MovieComponent上使用OnPush更改检测策略.

// app/movie.component.ts
@Component({ // ... changeDetection: ChangeDetectionStrategy.OnPush // 显示调用Onpush策略 }) export class MovieComponent { // ... }

  这将通知Angular:我们的组件仅依赖于它的输入(@Input),并且传递给它的任何对象都应该被认为是不可变的。这次当我们点击“changeActorProperties”按钮时,视图中没有任何变化。当用户单击该按钮时,将调用方法changeActorProperties并更新actor对象的属性。当更改检测分析绑定到AppComponent模板的属性时,它将看到与以前相同的数据:

Is slogan !== previousSlogan No, it's the same.
Is title !== previousTitle? No, it's the same.
Is actor !== previousActor? No, it's the same.

  但是这一次,我们明确地告诉Angular我们的组件只依赖于它的输入,并且它们都是不可变的。然后,Angular假定MovieComponent没有更改,并将跳过对该组件的检查。因为我们没有强制actor对象是不可变的,所以最终我们的模型与视图不同步. 

  让我们重新运行应用程序,但这次我们将单击“更改Actor对象”按钮。这一次,我们正在创建一个Actor类的新实例,并将其分配给this.actor对象。当更改检测分析绑定到AppComponent模板的属性时,它将找到:

Is slogan !== previousSlogan No, it's the same.
Is title !== previousTitle? No, it's the same.
Is actor !== previousActor? Yes, it has changed.

  因为更改检测现在知道actor对象已更改(它是一个新实例),它将继续并继续检查MovieComponent的模板以更新其视图。最后,我们的模板和模型是同步的。 

优缺点对比

  Default:

  1.   优点: 每一次有异步事件发生,Angular都会触发变更检测(脏检查),从根组件开始遍历其子组件,对每一个组件都进行变更检测,对dom进行更新
  2.   缺点: 有很多组件状态(state)没有发生变化,无需进行变更检测,进行没有必要的变更检测,如果你的应用程序中组件越多,性能问题会越来越明显.

  Onpush:

  1.   优点: 组件的变更检测(脏检查)完全依赖于组件的输入(@Input),只要输入值不变,就不会触发变更检测,也不会对其子组件进行变更检测,在组件很多的时候会有明显的性能提升
  2.   缺点:必须保证输入(@Input)是不可变的(可以用Immutable.js解决),就是每一次输入变化都必须是一个新的引用(js中object,array的可变性).