markForCheck()和detectChanges()有什么区别?

274

ChangeDetectorRef.markForCheck()ChangeDetectorRef.detectChanges()有什么区别?

我在SO上只找到了关于NgZone.run()的区别,但没有关于这两个函数的区别。

对于仅提供文档引用的答案,请举例说明何时选择其中一个而不是另一个。

5个回答

365

detectChanges() : void

检测变更,并更新视图。

这意味着,如果你的模型(类)中的任何内容已更改,但未反映在视图中,你可能需要通知 Angular 检测这些变化(检测本地变化)并更新视图。

可能的情况包括:

1- 变更检测器与视图分离(参见 detach

2- 发生了更新,但它不在 Angular Zone 内,因此 Angular 不知道它。

例如,当第三方函数已更新您的模型并且您希望在此之后更新视图。

 someFunctionThatIsRunByAThirdPartyCode(){
     yourModel.text = "new text";
 }

由于此代码在 Angular 的区域之外(很可能),您最有可能需要确保检测更改并更新视图,因此:

 myFunction(){
   someFunctionThatIsRunByAThirdPartyCode();

   // Let's detect the changes that above function made to the model which Angular is not aware of.
    this.cd.detectChanges();
 }

注意

有其他方法可以使上述代码运行,换句话说,有其他方法可以在Angular变更周期内实现该更改。

** 您可以将第三方函数包装在zone.run中:

 myFunction(){
   this.zone.run(this.someFunctionThatIsRunByAThirdPartyCode);
 }

你可以将该函数封装在 setTimeout 中:

myFunction(){
   setTimeout(this.someFunctionThatIsRunByAThirdPartyCode,0);
 }

3- 还有一些情况是你在 变更检测周期 结束后更新了模型,这种情况下会出现以下错误:

"Expression has changed after it was checked";

这通常意味着(来自Angular2的语言):

我看到你的模型发生了一个变化,这个变化是由我的接受方式之一(事件,XHR请求,setTimeout等)引起的,然后我运行了变更检测以更新你的视图,并完成了它,但接着,你的代码中还有另一个函数更新了模型,我不想再次运行变更检测,因为没有像AngularJS一样的脏检查了:D,我们应该使用单向数据流!

你肯定会遇到这个错误:P。

解决它的几种方法:

1- 正确的方法:确保更新在变更检测周期内(Angular2的更新是单向流,仅发生一次,之后不再更新模型,并将代码移动到更好的位置/时间)。

2- 懒惰的方式: 在更新后运行 detectChanges() 让 Angular2 开心,这绝对不是最好的方法,但由于你询问可能的场景,这是其中之一。

通过这种方式,你在说:我真诚地知道你运行了变更检测,但我希望你再次运行它,因为我必须在检查完成后即时更新一些东西。

3- 将代码放入 setTimeout 中,因为 setTimeout 已被 zone 打补丁并将在结束后运行 detectChanges


来自文档的引述

markForCheck() : void

将所有ChangeDetectionStrategy祖先标记为待检查。

这在你的组件的ChangeDetectionStrategyOnPush时大多数情况下是必需的。

OnPush本身意味着仅在发生以下任一情况时才运行更改检测:

1- 组件的@input之一已完全替换为新值,或者简单地说,如果@input属性的引用完全更改。

因此,如果你的组件的ChangeDetectionStrategyOnPush,并且你有:

   var obj = {
     name:'Milad'
   };

然后你可以这样更新/修改它:

  obj.name = "a new name";

这不会更新obj引用,因此变化检测不会运行,因此视图不会反映更新/突变。

在这种情况下,您必须手动告诉Angular检查并更新视图(markForCheck);

因此,如果您这样做:

  obj.name = "a new name";
你需要这样做:
  this.cd.markForCheck();

实际上,下面的操作会触发变更检测:

    obj = {
      name:"a new name"
    };

使用 {} 完全替换了先前的 obj;

2- 当事件触发时,例如点击或任何子组件发出事件。

事件如下:

  • 点击
  • 键盘弹起
  • 订阅事件
  • 等等。

简而言之:

  • 在 Angular 运行变更检测后更新模型,或者更新完全不在 Angular 的世界中时,使用 detectChanges()

  • 如果您正在使用 OnPush 并且通过改变某些数据或在 setTimeout 内更新模型来绕过 ChangeDetectionStrategy,则使用 markForCheck()


16
如果你改变了那个对象,视图不会被更新,即使运行detectChanges也不会生效,因为没有进行任何更改——这是不正确的。 detectChanges会更新视图。请参见此详细说明。 - Max Koretskyi
关于markForCheck在结论中的描述也不准确。这里有一个修改后的例子,来自这个问题,它不能检测到使用OnPush和markForCheck的对象更改。但是,如果没有OnPush策略,相同的例子将会起作用 - Estus Flask
是的,这些例子都是我写的。这个例子在启用OnPush后不按预期工作(我发现它之前被注释掉了)。使用OnPush时,title.name的更改不会传播到子组件,即使使用markForCheck()也不行。 - Estus Flask
@estus,当您更新父组件时,它不会更新其子组件,这是理所当然的。markForCheck将从根部更新到调用它的组件,不会向下传递给子组件。否则,只要放置一个markForCheck,框架就会更改整个树。 - Milad
4
太棒了,非常感谢! 我学到了好几件事情。@Milad - TetraDev
显示剩余6条评论

152

两者最大的区别在于detectChanges()会触发变更检测,而markForCheck()不会触发变更检测。

detectChanges

该方法用于对从您在其上触发detectChanges()的组件树运行变更检测。因此,变更检测将针对当前组件及其所有子组件运行。Angular将根组件树的引用保存在ApplicationRef中,并在发生任何异步操作时,通过包装方法tick()触发该根组件上的变更检测:

@Injectable()
export class ApplicationRef_ extends ApplicationRef {
  ...
  tick(): void {
    if (this._runningTick) {
      throw new Error('ApplicationRef.tick is called recursively');
    }

    const scope = ApplicationRef_._tickScope();
    try {
      this._runningTick = true;
      this._views.forEach((view) => view.detectChanges()); <------------------

view在这里是根组件视图。就像我在多个组件引导的影响中描述的那样,可以有许多根组件。

@milad解释了为什么你可能需要手动触发变更检测。

markForCheck

正如我所说,这个函数不会触发变更检测。它只是从当前组件向根组件上移,并将它们的视图状态更新为ChecksEnabled。以下是源代码:

export function markParentViewsForCheck(view: ViewData) {
  let currView: ViewData|null = view;
  while (currView) {
    if (currView.def.flags & ViewFlags.OnPush) {
      currView.state |= ViewState.ChecksEnabled;  <-----------------
    }
    currView = currView.viewContainerParent || currView.parent;
  }
}
组件的实际变化检测没有被安排,但当它在未来发生时(作为当前或下一个 CD 周期的一部分),即使父组件视图已分离变更检测器,也会对其进行检查。使用`cd.detach()`可以将变更检测器分离,或者通过指定`OnPush`变更检测策略来分离。所有原生事件处理程序都会标记所有父组件视图以进行检查。
这种方法经常在`ngDoCheck`生命周期钩子中使用。您可以在如果您认为 `ngDoCheck` 意味着您的组件正在被检查-请阅读本文中了解更多信息。
此外,请参阅Angular 中有关变更检测的全部内容获取更多详细信息。

6
为什么 detectChanges 作用于组件及其子组件,而 markForCheck 作用于组件及其祖先组件? - pablo
1
@pablo,这是有意为之的。我对其背后的原理并不是很熟悉。 - Max Koretskyi
1
@jerry,推荐的方法是使用异步管道,它在内部跟踪订阅,并在每个新值上触发markForCheck。因此,如果您没有使用异步管道,那么这可能是您应该使用的方法。但是,请记住,存储更新应作为某些异步事件的结果发生,以启动更改检测。这通常是情况。但也有例外情况 https://blog.angularindepth.com/do-you-still-think-that-ngzone-zone-js-is-required-for-change-detection-in-angular-16f7a575afef#f618 - Max Koretskyi
1
@MaxKoretskyiakaWizard 谢谢回复。是的,存储更新主要是由获取或在fetch之前和之后设置isFetching的结果。但我们不能总是使用async pipe,因为在subscribe内部通常有一些事情要做,比如调用setFromValues 进行一些比较。如果async本身调用markForCheck,那么如果我们自己调用它会有什么问题呢?但是,通常我们在ngOnInit中有2-3个或者更多的选择器来获取不同的数据...并且我们在所有这些选择器中都调用了markForCheck...这样可以吗? - jerry
2
在这种情况下,我感到困惑@MaxKoretskyi :( 我阅读了你的所有教程并看了你的演讲,但我仍然无法理解:为什么调用markForCheck()时会触发变更检测?我知道它将组件及其前任标记为脏状态,直到根组件,并且将在“当前或下一个CD周期”中进行检查。但是谁会触发下一个CD周期呢?我也尝试像您一样进行调试,但没有成功。还有其他方法吗?https://dev59.com/5r3pa4cB1Zd3GeqPfoOw - Tim
显示剩余11条评论

41

什么时候最好使用markForCheck? - hackp0int
当您确信CD将在之后运行并且不想进行额外的不必要CD时,请返回已翻译的文本。 - Stepan Suvorov

32

cd.detectChanges() 会立即运行当前组件及其子组件的变更检测。

cd.markForCheck() 不会立即运行变更检测,但会标记其祖先组件需要运行变更检测。下一次任何地方执行变更检测时,也会为这些被标记的组件运行变更检测。

  • 如果你想减少变更检测调用的次数,请使用cd.markForCheck()。通常情况下,更改会影响多个组件并且在某个地方会调用变更检测。你实际上是在说:当发生变更时,让我们确保也更新此组件。(在我编写的每个项目中,视图都会立即更新,但不是在每个单元测试中)。
  • 如果你不能确定cd.detectChanges() 当前是否正在运行变更检测,请使用cd.markForCheck()。在这种情况下,detectChanges()将出错。这可能意味着你尝试编辑祖先组件的状态,这违反了Angular变更检测的设计假设。
  • 如果在其他操作之前视图必须同步更新,可以使用detectChanges()markForCheck()可能无法及时更新视图。例如,在单元测试中,如果对视图产生影响,则可能需要手动调用fixture.detectChanges(),而在应用程序本身中则不需要。
  • 如果您正在更改具有更多祖先而不是后代的组件中的状态,则可以通过使用detectChanges()来提高性能,因为您不会不必要地在组件的祖先上运行变更检测。

  • -1

    最大的区别是markForCheck将检查当前组件的绑定,而不会调用子视图中的某些生命周期钩子(如DoCheck()),但detectChange()会!


    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接