Angular 2 change detection and ChangeDetectionStrategy.OnPush

I am trying to understand the mechanism of ChangeDetectionStrategy.OnPush .

What I collect from my readings is that change detection works by comparing the old value with the new value. This comparison will return false if the object reference has not changed.

However, there seem to be certain scenarios in which this “rule" bypasses. Could you explain how it all works?

+15
angular zonejs
Sep 30 '16 at 16:01
source share
3 answers

Edit: Here is the plunker I made that illustrates change detection using the OnPush strategy. Press "hello" as in 3 levels, then try other levels.

Well, since it took me the whole evening to understand that I had done a resume, so that everything could be settled in my head, and this could help future readers. So let's start by cleaning up some things:

Change comes from events

A component may have fields. These fields change only after some event, and only after that.

We can define the event as a mouse click, ajax request, setTimeout ...

Data streams from top to bottom

Angular data stream is a one-way street. This means that data is not transmitted from children to parents. Only from parent to children, for example, via the @Input tag. The only way to make the top component aware of some changes in the child is through an event . This brings us to:

Event trigger change detection

When an event occurs, the angular environment checks each component from top to bottom to see if they have changed. If any of them has changed, it updates the view accordingly.

Angular checks all components after the event has been fired. Say you have a click event on a component that is a component at the lowest level, that is, it has parents but no children. This click can initiate a change in the parent component using the emitter of an event, service, etc. angular does not know if the parents will change or not. This is why angular checks all components after the event has been fired by default.

To find out if they have changed the angular class ChangeDetector .

Change detector

Each component has a change detector class attached to it. It is used to check whether the component has changed after some event and to see if the view needs to be updated. When an event occurs (mouse click, etc.), this change detection process occurs for all components - by default -.

For example, if we have a ParentComponent:

 @Component({ selector: 'comp-parent', template:'<comp-child [name]="name"></comp-child>' }) class ParentComponent{ name:string; } 

We will have a change detector attached to the ParentComponent , which looks like this:

 class ParentComponentChangeDetector{ oldName:string; // saves the old state of the component. isChanged(newName){ if(this.oldName !== newName) return true; else return false; } } 

Change object properties

As you may have noticed, the isChanged method will return false if you change the property of the object. Really

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

Because when the property of an object can change without returning true in ChangeDetector isChanged() , angular will assume that each component below could also change. Therefore, it simply checks for change detection in all components.

Example: Here we have a component with a subcomponent. Although change detection will return false for the parent component, the child's view should be well updated.

 @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; } 

This is why the default behavior is to check all components. Because, although a subcomponent cannot change if its input has not changed, angular does not know for sure that the input has not changed. The object passed to it may be the same, but may have different properties.

OnPush Strategy

When a component is marked changeDetection: ChangeDetectionStrategy.OnPush , angular will assume that the input object has not changed if the reference to the object has not changed. This means that changing a property will not detect changes. Thus, the view will be incompatible with the model.

Example

This example is cool because it shows it in action. You have a parent component that, when clicked, changes the properties of the name of the input object. If you check the click() method inside the parent component, you will notice that it displays the property of the child component in the console. This property has changed. But you do not see it visually. This is because the view is not updated. Due to the OnPush strategy, the change detection process did not occur because the ref object did not change.

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

After clicking, the name still remains in the view, but not in the component itself




An event fired inside the component will trigger change detection.

Here we come to what confuses me in my original question. The component below is marked by the OnPush strategy, but the view is updated when it changes.

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"; } } 

So, we see that the input of the object did not change the link, and we use the OnPush strategy. Which can make us believe that it will not be updated. In fact, it is being updated.

As Gunter said in his answer, this is due to the fact that using the OnPush strategy, change detection occurs for the component if:

  • received related event (click) on the component itself.
  • updated @Input () (as in obj ref file)
  • | asynchronous pipe received an event
  • change detection was called manually

regardless of strategy.

References

+57
01 Oct '16 at 2:35
source share

*ngFor whether it has its own change detection. Each time changes are detected, NgFor gets its own ngDoCheck() method, and there NgFor checks if the contents of the array have changed.

There are no changes in your case, because the constructor is executed before Angular starts displaying the view.
If, for example, you added a button like

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

then a click will actually cause a change that NgFor should recognize.

With ChangeDetectionStrategy.OnPush detection of changes in your component will be triggered, because when changes are detected, OnPush is executed

  • associated event received (click)
  • a @Input() been updated with change detection
  • | async | async tube received an event
  • change detection was called manually
+15
Sep 30 '16 at 16:03
source share

To prevent Application.tick try disabling changeDetector:

 constructor(private cd: ChangeDetectorRef) { ngAfterViewInit() { this.cd.detach(); } 

Plunker

+6
Sep 30 '16 at 16:06
source share



All Articles