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?
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();
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.
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.