【问题标题】:Angular2 change detection misunderstanding - With plunkerAngular2 变更检测误区 - 用 plunker
【发布时间】:2017-03-12 22:20:15
【问题描述】:

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

这包括:

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

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

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


这里是 plunker:Angular2 Change detection playground

plunker 的快速解释:

很简单:

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

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

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

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

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


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

场景1:

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

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

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

场景 2:

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

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

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

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

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

当前行为: 检测到内部值更改...为什么?


结论:

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

  • 采用 OnPush 策略的组件在其输入发生更改时'检测到更改'即使其更改检测器已分离。
  • 具有 OnPush 策略的组件在每次输入更改时重新附加其更改检测器...

您如何看待这些结论?

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

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

【问题讨论】:

  • 不知道谁投了反对票。我认为这是一个非常有趣的问题,问题得到了完美的解释。
  • 我不确定,但我想我看到它提到detach() 分离了儿童的变化检测器,而不是组件本身。我自己还没有深入研究变化检测。
  • 文档说:“从变化检测器树中分离变化检测器。”使用默认 CD 策略附加/分离 CD 按预期与孩子一起工作

标签: angular plunker angular2-changedetection


【解决方案1】:

更新

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

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

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 类

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) 最后一次,编辑内部值输入,点击更改 internal:检测到变化,更新内部值……

如上所述,所有事件处理程序都包括markPathToRootAsCheckOnce。所以:

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

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

采用 OnPush 策略的组件重新附加其变更检测器 每次他们的输入改变...

最后我想说的是,你似乎是对的。

【讨论】:

  • 很好的解释。 Ben Nadel 的帖子中有一个答案解释了同样的事情:bennadel.com/blog/…。这引用了这个确切问题的下一个 github 问题:github.com/angular/angular/issues/9720
  • 很好的解释。似乎缺少图像。
  • 太好了,感谢您的出色回答以及相关的 github 问题!现在一切都清楚了。
猜你喜欢
  • 1970-01-01
  • 2017-04-02
  • 2017-06-05
  • 2017-06-03
  • 1970-01-01
  • 1970-01-01
  • 2016-09-25
  • 2017-10-03
  • 2017-06-27
相关资源
最近更新 更多