I'm trying to fully understand change detection with Angular2 final.
This include:
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:
The parent attribute can be passed to children components by either:
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:
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:
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.
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:
What do you think about those conclusions?
Can you explain this behavior in a better way?
Is this a bug or the desired behavior?
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.
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.
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.
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.
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
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 becomeChecked
.
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 toCheckOnce
.
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:
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 toCheckOnce
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)
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 toCheckOnce
.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With