我试图理解ChangeDetectionStrategy.OnPush
机制。
我从读数中收集到的是,更改检测通过将旧值与新值进行比较来工作。如果对象引用未更改,则该比较将返回 false。
但是,似乎在某些情况下会绕过该“规则”。你能解释一下它是如何工作的吗?
好的,因为这花了我整整一个晚上的时间来理解,所以我做了一份简历来解决我脑海中的所有问题,它可能会对未来的读者有所帮助。所以让我们从清理一些事情开始:
一个组件可能有字段。这些字段仅在某种事件之后才发生变化,并且仅在那之后。
我们可以将事件定义为鼠标点击、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;
}
这就是为什么默认行为是检查所有组件的原因。因为即使子组件在其输入没有改变的情况下不能改变,角度也不能确定它的输入没有真正改变。传递给它的对象可能是相同的,但它可能具有不同的属性。
当一个组件用 标记时changeDetection: ChangeDetectionStrategy.OnPush
,如果对象引用没有改变,angular 将假定输入对象没有改变。这意味着更改属性不会触发更改检测。因此视图将与模型不同步。
例子
这个例子很酷,因为它展示了这一点。您有一个父组件,单击时输入对象名称属性会更改。如果您检查click()
父组件中的方法,您会注意到它在控制台中输出子组件属性。该属性已更改..但是您无法直观地看到它。那是因为视图尚未更新。由于 OnPush 策略,更改检测过程没有发生,因为 ref 对象没有更改。
@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 策略,但视图会在更改时更新。
@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 策略,如果出现以下情况,则会对组件进行更改检测:
与策略无关。
*ngFor
它自己的变化检测。每次运行更改检测时,NgFor
都会调用其ngDoCheck()
方法并NgFor
检查数组的内容是否已更改。
在您的情况下,没有任何变化,因为构造函数是在 Angular 开始渲染视图之前执行的。
例如,如果您要添加一个按钮,例如
<button (click)="persons.push({name: 'dynamically added', id: persons.length})">add</button>
那么点击实际上会导致ngFor
必须识别的更改。
将在ChangeDetectionStrategy.OnPush
您的组件中运行更改检测,因为在运行OnPush
更改检测时
(click)
@Input()
已通过变更检测更新| async
管道收到一个事件为了防止Application.tick
尝试分离 changeDetector:
constructor(private cd: ChangeDetectorRef) {
ngAfterViewInit() {
this.cd.detach();
}
在 Angular 中,我们高度使用父子结构。在那里,我们使用@Inputs将数据从父级传递给子级。
在那里,如果孩子的任何祖先发生变化,变化检测将发生在该祖先的组件树中。
但是在大多数情况下,我们只需要在输入发生变化时更新孩子的视图(调用更改检测)。为此,我们可以使用OnPush ChangeDetectionStrategy并根据需要更改输入(使用不可变)。关联
默认情况下,每当应用程序中发生某些变化(所有浏览器事件、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 不想强迫你使用不可变的输入。