angular 2 变化检测和 ChangeDetectionStrategy.OnPush

angular 2 change detection and ChangeDetectionStrategy.OnPush

我正在尝试了解 ChangeDetectionStrategy.OnPush 机制。

我从阅读中收集到的是,变化检测是通过将旧值与新值进行比较来工作的。如果对象引用未更改,则该比较将 returns 为假。

然而,似乎在某些情况下 "rule" 被绕过了。你能解释一下这一切是如何运作的吗?

*ngFor 进行自己的变化检测。每次更改检测是 运行,NgFor 都会调用其 ngDoCheck() 方法,然后 NgFor 检查数组的内容是否已更改。

您的情况没有变化,因为构造函数在 Angular 开始呈现视图之前执行。
例如,如果您要添加一个按钮,例如

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

然后单击实际上会导致 ngFor 必须识别的更改。

使用 ChangeDetectionStrategy.OnPush 组件中的更改检测将是 运行 因为使用 OnPush 更改检测是 运行 when

  • 接收到绑定事件(click)
  • @Input() 已通过变更检测更新
  • | async 管道收到一个事件
  • 调用了更改检测"manually"

为了防止 Application.tick 尝试分离 changeDetector:

constructor(private cd: ChangeDetectorRef) {

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

Plunker

好吧,因为这花了我一晚上的时间才明白,所以我做了一份简历来解决我脑海中的一切,这可能对未来的读者有所帮助。那么让我们从清理一些东西开始:

变化来自事件

组件可能有字段。这些字段只会在某种事件发生后发生变化,而且只会在那之后发生变化。

我们可以定义一个事件为鼠标点击,ajax请求,setTimeout...

数据从上到下流动

Angular 数据流是单行道。这意味着数据不会从 children 流向 parents。仅从 parent 到 children 例如通过 @Input 标签。让上层组件知道 child 中某些更改的唯一方法是通过 事件 。这将我们带到:

事件触发器变化检测

当事件发生时,angular 框架从上到下检查每个组件以查看它们是否已更改。 如果有任何更改,它会相应地更新视图。

Angular 在触发事件后检查每个组件。假设您在最低级别的组件上有一个点击事件,这意味着它有 parents 但没有 children。该点击可能会通过事件发射器、服务等触发 parent 组件的更改。Angular 不知道 parent 是否会更改。这就是为什么 Angular 默认情况下会在事件触发后检查每个组件。

要查看它们是否已更改 angular 使用 ChangeDetector class.

变化检测器

每个组件都附加了一个变化检测器 class。它用于检查组件在某些事件后是否更改了状态,并查看是否应更新视图。当一个事件发生时(鼠标点击等)这个变化检测过程发生在所有组件上——默认情况下——。

例如,如果我们有一个 ParentComponent:

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

我们将在 ParentComponent 上附加一个更改检测器,如下所示:

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

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

正在更改 object 属性

您可能已经注意到,如果您更改 object 属性,isChanged 方法将 return 为 false。确实

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

因为当 object 属性 可以更改而 return 在 changeDetector isChanged() 中为真时,angular 将假定每个下面的组件也可能发生了变化。因此它将简单地检查所有组件中的变化检测。

示例: 这里我们有一个带有子组件的组件。虽然 parent 组件的变更检测 return 为假,但 child 的视图应该得到很好的更新。

@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;
}

这就是默认行为是检查所有组件的原因。因为即使子组件在其输入未更改的情况下也无法更改,angular 不能确定它的输入是否 真的 已更改。传递给它的 object 可能相同,但可能具有不同的属性。

OnPush 策略

当组件标记为 changeDetection: ChangeDetectionStrategy.OnPush 时,如果 object 引用未更改,angular 将假定输入 object 未更改。这意味着更改 属性 不会触发更改检测。因此视图将与模型不同步。

例子

这个例子很酷,因为它展示了这一点。您有一个 parent 组件,当单击该组件时,输入 object 名称属性会更改。 如果您检查 parent 组件中的 click() 方法,您会注意到它在控制台中输出 child 组件 属性。那个 属性 已经改变了..但是你不能在视觉上看到它。那是因为视图还没有更新。由于 OnPush 策略,更改检测过程没有发生,因为 ref object 没有更改。

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,
}

点击后视图中的名字仍然是 thierry,但组件本身却没有


组件内部触发的事件将触发更改检测。

现在我们来谈谈我最初的问题中让我感到困惑的地方。下面的组件标示了OnPush策略,但是视图在变化时更新..

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";
  }
}

所以这里我们看到 object 输入没有改变引用,我们正在使用策略 OnPush。这可能会让我们相信它不会更新。事实上它已经更新了。

正如 Gunter 在他的回答中所说,那是因为,使用 OnPush 策略,如果满足以下条件,组件就会发生变化检测:

  • 在组件本身上接收到(单击)绑定事件。
  • @Input() 已更新(如 ref obj 已更改)
  • |异步管道收到一个事件
  • 更改检测是“手动”调用的

不管策略如何。

链接

在angular中我们高度使用父子结构。我们使用 @Inputs 将数据从父级传递给子级。

在那里,如果子项的任何祖先发生变化,变化检测将在该祖先的组件树中发生。

但在大多数情况下,只有当输入发生变化时,我们才需要更新子视图(调用变化检测)。为此,我们可以使用 OnPush ChangeDetectionStrategy 并根据需要更改输入(使用不可变)。 LINK

默认情况下,每当应用程序发生变化(所有浏览器事件、XHR、Promises、计时器、间隔等...)时,Angular 运行s 对每个组件的变化检测是昂贵。当应用程序变大时,这可能会导致性能问题。

少数组件可能不需要针对上述所有类型的更改进行更改检测。因此,通过使用 onPush 策略,可以在以下场景

中对特定组件进行变更检测运行
- The Input reference changes(Immutable inputs)
- An event originated from the component or one of its children
- Run change detection explicitly
- Use the async pipe in the view

现在,有人可能会问为什么Angular不能将onPush作为默认策略。 答案是:Angular 不想强迫你使用不可变输入。