角度2变化检测和ChangeDetectionStrategy.OnPush

时间:2016-09-30 16:01:07

标签: angular zonejs

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

我从阅读材料中得到的结论是,通过将旧值与新值进行比较,可以进行更改检测。如果对象引用未更改,则该比较将返回false。

然而,似乎某些情况下绕过了“规则”。你能解释一下这一切是如何运作的吗?

5 个答案:

答案 0 :(得分:82)

好的,因为这让我花了整整一个晚上才知道我写了一份简历来解决我头脑中的一切,这可能会对未来的读者有所帮助。所以让我们从清除一些事情开始:

变更来自事件

组件可能包含字段。这些字段仅在某种事件发生后才会更改,并且仅在此之后发生。

我们可以将事件定义为鼠标单击,ajax请求,setTimeout ...

数据从上到下

角度数据流是一条单行道。这意味着数据不会从孩子流向父母。仅从父级到子级,例如通过@Input标记。让上层组件了解孩子的某些变化的唯一方法是通过事件。这将我们带到:

事件触发器更改检测

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

Angular在事件触发后检查每个组件。假设您在组件上有一个click事件,该组件是最低级别的组件,这意味着它具有父级但没有子级。该点击可以通过事件发射器,服务等触发父组件的更改.Angular不知道父母是否会改变。这就是Angular在默认情况下触发事件后检查每个组件的原因。

要查看他们是否更改了角度,请使用ChangeDetector类。

更改检测器

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

例如,如果我们有一个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;
    }
}

更改对象属性

您可能已经注意到,如果更改对象属性,则isChanged方法将返回false。确实

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

由于在changeDetector isChanged()中对象属性可以更改而不返回true,因此angular将假定每个下面的组件也可能已更改。因此,它只会检查所有组件中的变化检测。

示例:这里我们有一个带有子组件的组件。虽然更改检测将为父组件返回false,但应该更新子视图。

@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不确定它的输入是否真正改变了。传递给它的对象可能是相同的,但它可能具有不同的属性。

OnPush策略

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

示例

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

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

点击后,视图中的名称仍然很大,但组件本身却没有

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

在这里,我们在原始问题中遇到了令我困惑的事情。下面的组件标有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";
  }
}

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

正如Gunter在他的回答中所说,这是因为,使用OnPush策略,如果出现以下情况,则会对组件进行更改检测:

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

无论策略如何。

链接

答案 1 :(得分:19)

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

在你的情况下没有变化,因为构造函数在Angular开始渲染视图之前执行 例如,如果你想添加一个像

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

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

由于ChangeDetectionStrategy.OnPush更改检测在

运行时,您的组件中会发生OnPush更改检测
  • 收到绑定事件(click)
  • 通过更改检测更新@Input()
  • | async管道收到了一个活动
  • 更改检测已被调用&#34;手动&#34;

答案 2 :(得分:7)

防止Application.tick尝试分离changeDetector:

constructor(private cd: ChangeDetectorRef) {

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

Plunker

答案 3 :(得分:1)

在角度上,我们高度使用Parent-child结构。在那里,我们使用 @Inputs 将数据表单从父级传递到子级。

在那里,如果孩子的任何祖先发生了更改,则更改检测将在该祖先的组件树形式中发生。

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

答案 4 :(得分:0)

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

对于上述各种更改的少数组件,可能不需要更改检测。因此,通过使用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不想强迫您使用不可变的输入。