0%

angular 变更检测策略

Angular performs[执行] change detection on all components(from top to bottom) every time some thing changes in you app from something like a user event or data received from a network request.

当你的Angular应用中有某些比如用户触发事件或者异步接收到数据的变化时,Angular 会对所有组件(从上到下)执行更改检测。

Change detection is very performant[有效的], but as an app gets more complex and the amount of components grows, change detection will have to perform more and more work.

变更检测是非常有效的,但是当我们的应用变得复杂,同时增加了大量的组件,变更检测将不得不执行越来越多的工作。

There’s a way to circumvent[规避] that however and set the change detection strategy to OnPush on specific components.

有一种方法可以规避这种情况,那就是将特定组件的更改策略设置为OnPush

Doing this will instruct Angular to run change detection on these components and their sub-tree only when new references are passed to them versus when data is simply mutated.

这样做可以指示Angular仅在将新引用传递给他们以及仅对数据进行突变时才对这些组件以及其子树执行变更检测。

Simple Example

we take a component like this, index.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { Component } from '@angular/core';

@Component({
selector: 'app-index',
templateUrl: './index.component.html',
styleUrls: ['./index.component.less']
})
export class IndexComponent {

runtimes: number = 0;

foods = ['Bacon', 'Lettuce', 'Tomatoes'];

addFood(food: string) {
this.foods.push(food);
}

getRuntime() {
this.runtimes ++;
return this.runtimes;
}
}

index.component.html:

1
2
3
4
5
6
<input #newFood type="text" placeholder="enter a new food" />

<button (click)="addFood(newFood.value)">Add Food</button>

<p>check times: {{getRuntimes()}}</p>
<app-child [data]="foods"></app-child>

Our here’s our child component, child.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Component, OnInit, Input} from '@angular/core';
import { Observable } from 'rxjs';

@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.less'],
})
export class ChildComponent implements OnInit {

@Input() data: string[];

}

child.component.html:

1
2
3
4
5
<ul>
<li *ngFor="let item of foods">
{{item}}
</li>
</ul>

Everything works as expected and new food items get added to the list, thanks to our Input in the child component that receives it’s data from the parent. Now let’s set the change detection strategy in the child component to OnPush.

一切运行的非常完美,当有新的food被加入列表后,我们子组价中的Input会接收到从父组件中发出的数据变更。现在我们将子组件的变更检测策略变为OnPush.
child.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';
import { Observable } from 'rxjs';

@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements OnInit {

@Input() data: string[];

}

With this, things don’t seem to work anymore. The new data still gets pushed into our foods array in the parent components, but Angular don’t see a new reference for the data input and therefore doesn’t run change detection on the component.

运行代码后,发现并不能正常运作。在父组件中新的food的数据会被push进我们的foods数组中。但是Angular并没有发现有新的引用传递给子组件的输入参数,所以子组件也并没有执行变更检测。

To make it work again, the trick is to pass a completely new reference to our data input. This can be done with something like the instead of Array.push in our parent component’s addFood method.

要让它重新工作,一个技巧是可以给子组件的输入数据传递一个完整引用。可以将父组件中的addFood方法中的Array.push进行改造:
index.component.ts:

1
2
3
addFood(food) {
this.foods = [...this.foods, food];
}

with this variation[变异], we are not mutating[改变] the foods array anymore, but returning a completely new one, things are working aging in our child components! Angular detected a new reference to data, so it ran it’s change detection on the child component.

通过这个改变,我们并没有改变foods数组,但是我们返回了一个另一个全新的数组。可以看到可以正常在子组件中接收变更了。Angular检测到了data的一个新引用,所以就在子组件中运行了变更检测。

ChangeDetectionRef

When using a change detection strategy of OnPush, other than marking sure to pass new references every time something should change, we can also make use of the ChangeDetectorRef for complete control.

当使用了OnPush的变更检测策略,除了标记每次必须更改时都确保传递新引用之外,可以利用ChangeDetectionRef来进行完全控制。

ChangeDetectionRef.detectChanges()

We could for example keep mutating our data, and then have a button in our child component with a refresh button like this:

我们可以继续保持例子里面之前的更改数据,然后在子组件中创建一个带有刷新功能的按钮。
index.component.ts:

1
2
3
addFood(food) {
this.foods = [...this.foods, food];
}

child.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { Component, OnInit, Input, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core';

@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.less'],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ChildComponent implements OnInit {

refresh() {
this.cd.detectChanges();
}
}

child.component.html:

1
<button (click)="refresh()">refresh</button>

And now when we click the refresh button, Angular runs change detection on the component.

现在,我们点击这个子组件中的刷新按钮,Angular将会执行变更检测。

ChangeDetectionRef.markForCheck()

Let’s say you data input in actually an observable. Let’s demonstrate[演示] with example using a RxJS Behavior Subject.

假设我们的数据输入实际上是可观察的,那我们可以使用RxJS的BehaviorSubject来进行演示。
index.component.ts:

1
2
3
4
5
6
7
8
export class IndexComponent {

foods = new BehaviorSubject(['Bacon', 'Letuce', 'Tomatoes']);

addFood(food: string) {
this.foods.next([food]);
}
}

And we subscribe to it in the OnInit hook in our child component. We’ll add our food items to a food array here.

我们可以在子组件的OnInit钩子函数中订阅数据的变更,同时将新的food添加进foods数组中。
child.component.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export class ChildComponent implements OnInit {

@Input() data: Observable<any>;

foods: string[] = [];

constructor(
private cd: ChangeDetectorRef,
) { }

ngOnInit() {
this.data.subscribe(food => {
this.foods = [...this.foods, ...food];
this.cd.markForCheck();
})
}
}

This would normally work right out of the box the same as our initial examlple, when the new data mutates our data observable, we call ChangeDetectorRefs’s markForCheck to run Angular change detection.

这与我们的初始示例一样,可以立即使用,当新数据变为可观察的数据时,我们需要调用ChangeDetectionRefmarkeForCheck函数来运行Angular的变更检测。
child.component.html:

1
2
3
4
5
<ul>
<li *ngFor="let item of foods">
{{item}}
</li>
</ul>

markForCheck instructs[指示] Angular that this particular input should trigger change detection when mutated.

markForCheck指示Angular该特定输入在发生变更时要触发变更检测。

Yet another powerfull thing you can do with ChangeDetectionRef is to completely[完全的] detach[分离] and reattach[重新连接] change detection manually[手动的] with the detach and reattach methods.

也可以使用另一项强大的功能,可以手动的通过detachreattach函数来完全的分离、重新连接变更检测。

LINK: https://alligator.io/angular/change-detection-strategy/


通过这篇英文文章,学习angular的变更检测的使用。确实在这方面了解不多,以前也遇到过在页面模板中写个方法进行console,发现一直在输出,当时也没仔细想,只是觉得是angular在运行变更检测,也没怎么放在心上。现在组件分离搞的比较多,一个页面可能十多个组件,这种情况就需要考虑变更策略的方式了,还是得多想,多看,不要囫囵吞枣。over~

码字辛苦,打赏个咖啡☕️可好?💘