Angular2 变化检测误解 - With plunker

Angular2 change detection misunderstanding - With plunker

我正在尝试完全理解 Angular2 final 的变化检测。

这包括:

我认为我已经对这些概念有了一个非常清晰的概述,但为了确保我的假设正确,我写了一个小插件来测试它们。

我对全局的一般理解是正确的,但在某些情况下,我有点迷茫。


这里是 plunker: Angular2 Change detection playground

plunker的快速解释:

很简单:

parent 属性可以通过以下任一方式传递给 children 组件:

对于每个 child 组件,可以附加或分离 ChangeDetector。 ("detach()""reattach()" 按钮)

OnPush child 有一个额外的内部 属性 可以编辑, 并且可以显式应用更改检测("detectChanges()" 按钮)


以下是我无法解释的行为的场景:

场景 1:

  1. 分离 OnPush Children 和默认 Children 的更改检测器(在两个组件上单击“detach()”)
  2. 编辑 parent 属性名字和姓氏
  3. 点击“Change obj”将修改后的属性传递给children

预期行为: 我希望 BOTH children 不会被更新,因为它们都分离了变化检测器。

当前行为: 默认 child 未更新,但 OnPush child 已更新.. WHY? 它不应该因为它的 CD 是分离的...

场景2:

  1. 分离 OnPush 组件的 CD
  2. 编辑其内部值输入并单击更改内部:没有任何反应,因为 CD 已分离,因此未检测到更改。 . 好
  3. 单击 detectChanges():检测到更改并更新视图。到目前为止一切顺利。
  4. 再次编辑内部值输入并点击更改内部:再次,没有任何反应,因为CD是分离的,所以未检测到变化.. OK
  5. 编辑 parent 属性名字和姓氏。
  6. 点击“Change obj”将修改后的属性传递给children

预期行为: OnPush children 根本不应该更新,再一次因为它的 CD 是分离的......CD 根本不应该在这个组件上发生

当前行为: 值和内部值都已更新,像完整 CD 一样的接缝应用于此组件。

  1. 最后一次,编辑内部值 输入并单击更改内部:检测到更改,并更新内部值。 ..

预期行为: 不应更新内部值,因为 CD 仍处于分离状态

当前行为: 检测到内部值变化... 为什么?


结论:

根据这些测试,我得出以下结论,这对我来说很奇怪:

您如何看待这些结论?

你能用更好的方式解释这种行为吗?

这是错误还是预期的行为?

更新

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

因为 Angular 4.1.1 (2017-05-04) OnPush 应该尊重 detach()

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

旧版本

有很多关于变化检测如何工作的未记录的东西。

我们应该注意三个主要的 changeDetection 状态 (cdMode):

1) CheckOnce - 0

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

AppViewclass

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

2) 已检查 - 1

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

3) 分离 - 3

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

这里是使用Detached的地方

AppViewclass

跳过内容检查

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);
  }
}

跳过视图检查

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);
  }
}

跳过将 cdMode 更改为 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;
  }
}

注意:markPathToRootAsCheckOnce 在您视图的所有事件处理程序中是 运行:

因此,如果将状态设置为 Detached,那么您的视图将不会改变。

那么如何运作OnPush策略

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

让我们看看它在您的示例中的样子:

父工厂 (AppComponent)

然后再次回到 AppView class:

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

场景一

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

因此 OnPush.dectectChanges 正在开火。

结论如下:

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.

场景2

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 ...

正如我上面提到的,所有事件处理程序都包括 markPathToRootAsCheckOnce。所以:

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

如您所见,OnPush 策略和 ChangeDetector 管理一个 属性 - cdMode

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

最后我想说你是对的。