8
votes

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?

1
No idea who downvoted. I think this is a pretty interesting question and the problem is perfectly explained.Günter Zöchbauer
I'm not sure but I think I saw it mentioned that detach() detaches the change detector for children, but not the component itself. I didn't dive that deep into change detection yet myself.Günter Zöchbauer
The doc say: 'Detaches the change detector from the change detector tree.' Attaching/detaching CD work as expected with the child using default CD strategyClement

1 Answers

13
votes

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.