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();
}
好吧,因为这花了我一晚上的时间才明白,所以我做了一份简历来解决我脑海中的一切,这可能对未来的读者有所帮助。那么让我们从清理一些东西开始:
变化来自事件
组件可能有字段。这些字段只会在某种事件发生后发生变化,而且只会在那之后发生变化。
我们可以定义一个事件为鼠标点击,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 没有更改。
@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";
}
}
所以这里我们看到 object 输入没有改变引用,我们正在使用策略 OnPush。这可能会让我们相信它不会更新。事实上它已经更新了。
正如 Gunter 在他的回答中所说,那是因为,使用 OnPush 策略,如果满足以下条件,组件就会发生变化检测:
- 在组件本身上接收到(单击)绑定事件。
- @Input() 已更新(如 ref obj 已更改)
- |异步管道收到一个事件
- 更改检测是“手动”调用的
不管策略如何。
链接
- https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
- http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
- https://angular-2-training-book.rangle.io/handout/change-detection/change_detector_classes.html
- https://www.youtube.com/watch?v=X0DLP_rktsc
在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 不想强迫你使用不可变输入。
我正在尝试了解 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();
}
好吧,因为这花了我一晚上的时间才明白,所以我做了一份简历来解决我脑海中的一切,这可能对未来的读者有所帮助。那么让我们从清理一些东西开始:
变化来自事件
组件可能有字段。这些字段只会在某种事件发生后发生变化,而且只会在那之后发生变化。
我们可以定义一个事件为鼠标点击,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 没有更改。
@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";
}
}
所以这里我们看到 object 输入没有改变引用,我们正在使用策略 OnPush。这可能会让我们相信它不会更新。事实上它已经更新了。
正如 Gunter 在他的回答中所说,那是因为,使用 OnPush 策略,如果满足以下条件,组件就会发生变化检测:
- 在组件本身上接收到(单击)绑定事件。
- @Input() 已更新(如 ref obj 已更改)
- |异步管道收到一个事件
- 更改检测是“手动”调用的
不管策略如何。
链接
- https://hackernoon.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206f
- http://blog.angular-university.io/how-does-angular-2-change-detection-really-work/
- https://angular-2-training-book.rangle.io/handout/change-detection/change_detector_classes.html
- https://www.youtube.com/watch?v=X0DLP_rktsc
在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 不想强迫你使用不可变输入。