8

我正在尝试使用 Angular2 final 完全理解更改检测。

这包括:

  • 处理变更检测策略
  • 将变化检测器与组件连接和分离。

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

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


这是plunker:Angular2 Change detection playground

plunker的快速解释:

很简单:

  • 一个父组件,您可以在其中编辑一个属性,该属性将传递给两个子组件:
  • 在更改检测策略设置为 OnPush 的子节点上
  • 在更改检测策略设置为默认值的子项上

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

  • 更改整个属性对象,并创建一个新对象(“更改 obj ”按钮)(触发对 OnPush 子项的更改检测)
  • 更改属性对象内的成员(“更改内容”按钮)(不会触发 OnPush 子项上的更改检测)

对于每个子组件,可以附加或分离 ChangeDetector。(“分离()”“重新连接()”按钮)

OnPush 子有一个可以编辑的附加内部属性,并且可以显式应用更改检测(“detectChanges()”按钮)


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

场景1:

  1. 分离 OnPush Children 和 Default Children 的更改检测器(单击两个组件上的“ detach() ”)
  2. 编辑父属性 firstname 和 lastname
  3. 点击“ Change obj ”将修改后的属性传递给孩子

预期行为: 我希望两个孩子都不会被更新,因为他们的变化检测器都是分离的。

当前行为: 默认子级未更新,但 OnPush 子级已更新..为什么? 它不应该,因为它的 CD 已分离...

场景2:

  1. 为 OnPush 组件分离 CD
  2. 编辑其内部值输入并单击更改内部:没有任何反应,因为 CD 已分离,因此未检测到更改...确定
  3. 单击detectChanges():检测到更改并更新视图。到现在为止还挺好。
  4. 再次,编辑内部值输入并单击更改内部:再次,没有任何反应,因为 CD 已分离,因此未检测到更改.. OK
  5. 编辑父属性 firstname 和 lastname。
  6. 点击“ Change obj ”将修改后的属性传递给孩子

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

当前行为: 值和内部值都被更新,像一张完整的 CD 一样的接缝被应用到这个组件上。

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

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

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


结论:

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

  • 具有 OnPush 策略的组件在其输入更改时会“检测到更改” ,即使其更改检测器已分离。
  • 每次输入更改时,具有 OnPush 策略的组件都会重新附加更改检测器...

您如何看待这些结论?

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

这是一个错误还是期望的行为?

4

1 回答 1

13

更新

具有 OnPush 策略的组件在其输入更改时会“检测到更改”,即使其更改检测器已分离。

由于Angular 4.1.1(2017-05-04)OnPush应该尊重detach()

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

旧版

有很多关于变更检测如何工作的无证资料。

我们应该知道三个主要的changeDetection 状态cdMode):

1) CheckOnce - 0

CheckedOnce意味着调用 detectChanges 后,变化检测器的模式将变为Checked.

AppView 类

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

2)检查- 1

Checked意味着应该跳过更改检测器,直到其模式更改为CheckOnce.

3)分离式- 3

Detached意味着变化检测子树不是主树的一部分,应该被跳过。

这里Detached是使用的地方

AppView 类

跳过内容检查

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

跳过更改cdModeCheckOnce

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表示变化检测器的模式将CheckOnce 在水合期间设置为。

编译器/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 类

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

方案 1

1) 分离 OnPush Children 和 Default Children 的更改检测器(单击两个组件上的“detach()”)

OnPush.cdMode - Detached

3)点击“Change obj”将修改后的属性传递给孩子

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是开火。

以下是结论:

具有策略的组件OnPush在其输入更改时会“检测到更改”,即使其更改检测器已分离。此外,它将视图的状态更改为CheckOnce

情景2

1) 为 OnPush 组件分离 CD

OnPush.cdMode - Detached

6)点击“Change obj”将修改后的属性传递给孩子

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

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

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

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

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

每次输入更改时,具有 OnPush 策略的组件都会重新附加更改检测器...

总之,我想说这似乎你是对的。

于 2016-10-30T20:47:24.723 回答