Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
195 views
in Technique[技术] by (71.8m points)

zonejs - angular 2 change detection and ChangeDetectionStrategy.OnPush

I'm trying to understand the ChangeDetectionStrategy.OnPush mechanism.

What I gather from my readings is that a change detection works by comparing the old value to the new value. That comparison will returns false if the object reference hasn't changed.

However there seems to be certain scenarios where that "rule" is bypassed. Could you explain how does it all work ?

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

Okay, since this took me a whole evening to understand I made a resume to settle everything in my head and it might help future readers. So let's start by clearing some things up:

Changes come from events

A component might have fields. Those fields only change after some sort of event, and only after that.

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

Data flows from top to bottom

Angular data flow is a one way street. That means that data doesn't flow from children to parents. Only from parent to children for instance via the @Input tag. The only way to make a upper component aware of some change in a child is through an event. Which brings us to:

Event trigger change detection

When an event happens the angular framework check every component from top to bottom to see if they have changed. If any has changed, it updates the view accordingly.

Angular checks every components after an event has been fired. Say you have a click event on a component that is the component at the lowest level, meaning it has parents but no children. That click could trigger a change in a parent component via an event emitter, a service, etc.. Angular doesn't know if the parents will change or not. That is why Angular checks every components after an event has been fired by default.

To see if they have changed angular use the ChangeDetector class.

Change Detector

Every component has a change detector class attached to it. It is used to check if a component has changed state after some event and to see if the view should be updated. When an event happen (mouse click, etc) this change detection process happens for all the 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 that 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;
    }
}

Changing object properties

As you might have notice the isChanged method will return false if you change an object property. Indeed

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

Since when an object property can change without returning true in the changeDetector isChanged(), angular will assume that every below component might have changed as well. So it will simply check for change detection in all components.

Example: here we have a component with a sub component. While the change detection will return false for the parent component, the view of the child should very well be 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;
}

That is why the default behavior is to check all components. Because even though a sub component cannot change if its input haven't changed, angular doesn't know for sure it's input haven't really changed. The object passed to it might be the same but it could have different properties.

OnPush strategy

When a component is marked with changeDetection: ChangeDetectionStrategy.OnPush, angular will assume that the input object did not change if the object reference did not change. Meaning that changing a property won't trigger change detection. Thus the view will be out of sync with the model.

Example

This example is cool because it shows this in action. You have a parent component that when clicked the input object name properties is changed. If you check the click() method inside the parent component you will notice it outputs the child component property in the console. That property has changed..But you can't see it visually. That's because the view hasn't been updated. Because of the OnPush strategy the change detection process didn't happen because the ref object didn't 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 the click the name is still thierry in the view but not in the component itself


An event fired inside a component will trigger change detection.

Here we come to what confused me in my original question. The component below is marked with the OnPush strategy, yet 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 here we see that the object input hasn't changed reference and we are using strategy OnPush. Which might lead us to believe that it won't be updated. In fact it is updated.

As Gunter said in his answer, that is because, with the OnPush strategy the change detection happens for a component if:

  • a bound event is received (click) on the component itself.
  • an @Input() was updated (as in the ref obj changed)
  • | async pipe received an event
  • change detection was invoked "manually"

irregardless of the strategy.

Links


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...