41

我试图理解ChangeDetectionStrategy.OnPush机制。

我从读数中收集到的是,更改检测通过将旧值与新值进行比较来工作。如果对象引用未更改,则该比较将返回 false。

但是,似乎在某些情况下会绕过该“规则”。你能解释一下它是如何工作的吗?

4

5 回答 5

109

好的,因为这花了我整整一个晚上的时间来理解,所以我做了一份简历来解决我脑海中的所有问题,它可能会对未来的读者有所帮助。所以让我们从清理一些事情开始:

变化来自事件

一个组件可能有字段。这些字段仅在某种事件之后才发生变化,并且仅在那之后。

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

数据从上到下流动

Angular 数据流是单向的。这意味着数据不会从孩子流向父母。仅从父母到孩子,例如通过@Input标签。让上层组件知道子组件的某些变化的唯一方法是通过事件。这使我们:

事件触发变化检测

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

Angular 在事件触发后检查每个组件。假设您在一个组件上有一个点击事件,该组件是最低级别的组件,这意味着它有父级但没有子级。该点击可能会通过事件发射器、服务等触发父组件的更改。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

由于当对象属性可以更改而不在 中返回 true 时changeDetector isChanged(),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;
}

这就是为什么默认行为是检查所有组件的原因。因为即使子组件在其输入没有改变的情况下不能改变,角度也不能确定它的输入没有真正改变。传递给它的对象可能是相同的,但它可能具有不同的属性。

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

单击后,名称在视图中仍然是 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";
  }
}

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

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

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

与策略无关。

链接

于 2016-10-01T02:35:57.637 回答
23

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

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

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

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

将在ChangeDetectionStrategy.OnPush您的组件中运行更改检测,因为在运行OnPush更改检测时

  • 收到绑定事件(click)
  • an@Input()已通过变更检测更新
  • | async管道收到一个事件
  • “手动”调用更改检测
于 2016-09-30T16:03:57.590 回答
7

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

constructor(private cd: ChangeDetectorRef) {

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

普朗克

于 2016-09-30T16:06:41.657 回答
1

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

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

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

于 2018-07-18T04:18:56.163 回答
0

默认情况下,每当应用程序中发生某些变化(所有浏览器事件、XHR、Promise、计时器、间隔等)时,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 不想强迫你使用不可变的输入。

于 2020-04-18T21:23:27.290 回答