Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Angular2 change detection misunderstanding - With plunker

I'm trying to fully understand change detection with Angular2 final.

This include:

  • Dealing with change detection strategies
  • Attaching and detaching change detector from a component.

I thought I already got a pretty clear overview of those concepts, but to make sure my assumptions where right, I wrote a small plunkr to test them.

My general understanding about that where globally right, but in some situations, I get a little bit lost.


Here is the plunker: Angular2 Change detection playground

Quick explanation of the plunker:

Pretty simple:

  • One parent component where you can edit one attribute that will get passed down to two children components:
  • On child with change detection strategy set to OnPush
  • On child with change detection strategy set to Default

The parent attribute can be passed to children components by either:

  • Changing the whole attribute object, and creating a new one ("Change obj" button) (which trigger change detection on the OnPush child)
  • Changing the members inside the attribute object ("Change content" button) (which do not trigger change detection on the OnPush child)

For each child component, ChangeDetector can be attached or detached. ("detach()" and "reattach()" buttons)

OnPush child have an additional internal property that can be edited, and change detection can be explicitly applied ("detectChanges()" button)


Here are the scenarios where I get behaviours that I cannot explain:

Scenario1:

  1. Detach Change detector of OnPush Children and Default Children (click "detach()" on both components)
  2. Edit the parent attribute firstname and lastname
  3. Click "Change obj" to pass the modified attribute to the children

Expected behavior: I expect BOTH children NOT to be updated, because they both have their change detector detached.

Current behavior: Default child is not updated, but OnPush child is updated .. WHY? It shouldn't because its CD is detached ...

Scenario2:

  1. Detach CD for the OnPush component
  2. Edit its internal value input and click change internal: Nothing happen, because CD is detached, so change is not detected... OK
  3. Click detectChanges(): changes are detected and the view is updated. So far so good.
  4. Once again, edit the internal value input and click change internal: Once again, nothing happen, because CD is detached, so change is not detected.. OK
  5. Edit the parent attribute firstname and lastname.
  6. Click "Change obj" to pass the modified attribute to the children

Expected behavior: OnPush children should NOT be updated at ALL, once again because its CD is detached...CD should not happen at all on this component

Current behavior: Both the value and the internal values are updated, seams like a full CD is applied to this component.

  1. For the last time, edit the internal value input and click change internal: Change is detected, and internal value is updated...

Expected behavior: Internal value should NOT be updated because CD is still detached

Current behavior: Internal value change is detected... WHY?


Conclusions:

According to those tests I conclude the following, which seams strange to me:

  • Component with OnPush strategy get 'changed detected' when their input changes, EVEN IF their change detector is detached.
  • Component with OnPush strategy get their change detector re attached each time their input changed...

What do you think about those conclusions?

Can you explain this behavior in a better way?

Is this a bug or the desired behavior?

like image 536
Clement Avatar asked Oct 30 '16 17:10

Clement


People also ask

What triggers OnPush change detection?

But with OnPush strategy, the change detector is only triggered if the data passed on @Input() has a new reference. This is why using immutable objects is preferred, because immutable objects can be modified only by creating a new object reference.

What is OnPush strategy in Angular?

The main idea behind the OnPush strategy manifests from the realization that if we treat reference types as immutable objects, we can detect if a value has changed much faster. When a reference type is immutable, this means every time it is updated, the reference on the stack memory will have to change.

What does ChangeDetectorRef detectChanges () do?

detectChanges() on ChangeDetectorRef which runs change detection on this view and its children by keeping the change detection strategy in mind. It can be used in combination with detach() to implement local change detection checks.

How does Angular detect change detection?

To run the change detector manually: Inject ChangeDetectorRef service in the component. Use markForCheck in the subscription method to instruct Angular to check the component the next time change detectors run. On the ngOnDestroy() life cycle hook, unsubscribe from the observable.


1 Answers

Update

Component with OnPush strategy get 'changed detected' when their input changes, EVEN IF their change detector is detached.

Since Angular 4.1.1 (2017-05-04) OnPush should respect detach()

https://github.com/angular/angular/commit/acf83b9

Old version

There are a lot of undocumented stuff about how change detection works.

We should be aware about three main changeDetection statuses (cdMode):

1) CheckOnce - 0

CheckedOnce means that after calling detectChanges the mode of the change detector will become Checked.

AppView class

detectChanges(throwOnChange: boolean): void {
  ...
  this.detectChangesInternal(throwOnChange);
  if (this.cdMode === ChangeDetectorStatus.CheckOnce) {
    this.cdMode = ChangeDetectorStatus.Checked; // <== this line
  }
  ...
}

2) Checked - 1

Checked means that the change detector should be skipped until its mode changes to CheckOnce.

3) Detached - 3

Detached means that the change detector sub tree is not a part of the main tree and should be skipped.

Here are places where Detached is used

AppView class

Skip content checking

detectContentChildrenChanges(throwOnChange: boolean) {
  for (var i = 0; i < this.contentChildren.length; ++i) {
    var child = this.contentChildren[i];
    if (child.cdMode === ChangeDetectorStatus.Detached) continue; // <== this line
    child.detectChanges(throwOnChange);
  }
}

Skip view checking

detectViewChildrenChanges(throwOnChange: boolean) {
  for (var i = 0; i < this.viewChildren.length; ++i) {
    var child = this.viewChildren[i];
    if (child.cdMode === ChangeDetectorStatus.Detached) continue; // <== this line
    child.detectChanges(throwOnChange);
  }
}

Skip changing cdMode to CheckOnce

markPathToRootAsCheckOnce(): void {
  let c: AppView<any> = this;
  while (isPresent(c) && c.cdMode !== ChangeDetectorStatus.Detached) { // <== this line
    if (c.cdMode === ChangeDetectorStatus.Checked) {
      c.cdMode = ChangeDetectorStatus.CheckOnce;
    }
    let parentEl =
        c.type === ViewType.COMPONENT ? c.declarationAppElement : c.viewContainerElement;
    c = isPresent(parentEl) ? parentEl.parentView : null;
  }
}

Note: markPathToRootAsCheckOnce is running in all event handlers of your view:

enter image description here

So if set status to Detached then your view won't be changed.

Then how works OnPush strategy

OnPush means that the change detector's mode will be set to CheckOnce during hydration.

compiler/src/view_compiler/property_binder.ts

const directiveDetectChangesStmt = isOnPushComp ?
   new o.IfStmt(directiveDetectChangesExpr, [compileElement.appElement.prop('componentView')
           .callMethod('markAsCheckOnce', [])
           .toStmt()]) : directiveDetectChangesExpr.toStmt();

https://github.com/angular/angular/blob/2.1.2/modules/%40angular/compiler/src/view_compiler/property_binder.ts#L193-L197

Let's see how it looks in your example:

Parent factory (AppComponent)

Enter image description here

And again back to the AppView class:

markAsCheckOnce(): void { this.cdMode = ChangeDetectorStatus.CheckOnce; }

Scenario 1

1) Detach Change detector of OnPush Children and Default Children (click "detach()" on both components)

OnPush.cdMode - Detached

3) Click "Change obj" to pass the modified attribute to the children

AppComponent.detectChanges
       ||
       \/
//if (self._OnPush_35_4.detectChangesInInputProps(self,self._el_35,throwOnChange)) {
//  self._appEl_35.componentView.markAsCheckOnce();
//}
OnPush.markAsCheckOnce
       ||
       \/
OnPush.cdMode - CheckOnce
       ||
       \/
OnPush.detectChanges
       ||
       \/
OnPush.cdMode - Checked

Therefore OnPush.dectectChanges is firing.

Here is conclusion:

Component with OnPush strategy get 'changed detected' when their input changes, EVEN IF their change detector is detached. Moreover It changes view's status to CheckOnce.

Scenario2

1) Detach CD for the OnPush component

OnPush.cdMode - Detached

6) Click "Change obj" to pass the modified attribute to the children

See 3) from scenario 1 => OnPush.cdMode - Checked

7) For the last time, edit the internal value input and click change internal: Change is detected, and internal value is updated ...

As I mentioned above, all event handlers includes markPathToRootAsCheckOnce. So:

markPathToRootAsCheckOnce
        ||
        \/
OnPush.cdMode - CheckOnce
        ||
        \/
OnPush.detectChanges
        ||
        \/
OnPush.cdMode - Checked

As you can see OnPush strategy and ChangeDetector manage one property - cdMode

Component with OnPush strategy get their change detector re attached each time their input changed ...

In conclusion I want to say that seems you're right.

like image 140
yurzui Avatar answered Oct 01 '22 06:10

yurzui