为什么我们需要 `ngDoCheck`

Why do we need `ngDoCheck`

我似乎无法弄清楚为什么我需要 ngDoCheck 生命周期挂钩,而不是简单的通知,特别是在其中编写代码如何在变更检测方面产生影响。我发现的大多数示例都是无用的示例,例如 this one,但带有大量日志记录功能。

此外,在生成的 类 中,我没有看到它被用于除简单通知以外的其他用途:

conmponent/wrapper.ngfactory.js

Wrapper_AppComponent.prototype.ngDoCheck = function(view,el,throwOnChange) {
  var self = this;
  var changed = self._changed;
  self._changed = false;
  if (!throwOnChange) {
    if (changed) {
      jit_setBindingDebugInfoForChanges1(view.renderer,el,self._changes);
      self._changes = {};
    }
    self.context.ngDoCheck(); <----------- this calls ngDoCheck on the component
                                               but the result is not used 
                                               anywhere and no params are passed
  }
  return changed;
};

DoCheck 界面用于手动检测 angular 变化检测忽略的变化。当您更改组件的 ChangeDetectionStrategy,但您知道对象的一个​​ 属性 会发生变化时,可能会有一个用途。

检查这一变化比让 changeDetector 运行 遍历整个组件更有效

let obj = {
  iChange = 'hiii'
}

如果您在模板中使用 obj.iChange,如果此值更改,angular 将不会检测到它,因为 obj 本身的引用不会更改。您需要实现一个 ngDoCheck 来检查值是否已更改,并在组件的 changeDetector 上调用 detectChanges

来自 angular 关于 DoCheck

的文档

While the ngDoCheck hook can detect when the hero's name has changed, it has a frightful cost. This hook is called with enormous frequency — after every change detection cycle no matter where the change occurred. It's called over twenty times in this example before the user can do anything.

Most of these initial checks are triggered by Angular's first rendering of unrelated data elsewhere on the page. Mere mousing into another input box triggers a call. Relatively few calls reveal actual changes to pertinent data. Clearly our implementation must be very lightweight or the user experience will suffer.

测试示例

@Component({
   selector: 'test-do-check',
   template: `
      <div [innerHtml]="obj.changer"></div>
   `, 
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class TestDoCheckComponent implements DoCheck, OnInit {
    public obj: any = {
       changer: 1
    };
    
    private _oldValue: number = 1;
   
    constructor(private _changeRef: ChangeDetectorRef){}
    
    ngOnInit() {
       setInterval(() => {
          this.obj.changer += 1;
       }, 1000);
    }
    
    ngDoCheck() {
       if(this._oldValue !== this.obj.changer) {
          this._oldValue = this.obj.changer;

               //disable this line to see the counter not moving
           this._changeRef.detectChanges();
       }
    }
}

这篇很棒的文章 If you think ngDoCheck means your component is being checked — read this article 深入解释了错误。

本回答内容基于angular版本2.x.x。对于最新版本 4.x.x,请参阅 this post

Internet 上没有关于更改检测内部工作原理的任何内容,所以我不得不花大约一周的时间调试源代码,所以这个答案在细节上会非常技术化。

angular 应用程序是 views 的树(AppView class 由编译器生成的特定组件 class 扩展)。每个视图都有一个位于 cdMode 属性 中的变化检测模式。 cdMode 的默认值为 ChangeDetectorStatus.CheckAlways,即 cdMode = 2.

当一个变化检测周期运行s时,每个父视图检查它是否应该对子视图进行变化检测here:

      detectChanges(throwOnChange: boolean): void {
        const s = _scope_check(this.clazz);
        if (this.cdMode === ChangeDetectorStatus.Checked ||
            this.cdMode === ChangeDetectorStatus.Errored)
          return;
        if (this.cdMode === ChangeDetectorStatus.Destroyed) {
          this.throwDestroyedError('detectChanges');
        }
        this.detectChangesInternal(throwOnChange); <---- performs CD on child view

其中 this 指向 child 视图。因此,如果 cdModeChangeDetectorStatus.Checked=1,则会因为这一行而跳过直接子项及其所有后代的更改检测。

    if (this.cdMode === ChangeDetectorStatus.Checked ||
            this.cdMode === ChangeDetectorStatus.Errored)
          return;

changeDetection: ChangeDetectionStrategy.OnPush 所做的只是将 cdMode 设置为 ChangeDetectorStatus.CheckOnce = 0,因此在第一次 运行 变化检测之后,子视图将具有其 cdMode设置为 ChangeDetectorStatus.Checked = 1 因为 this code:

    if (this.cdMode === ChangeDetectorStatus.CheckOnce) 
         this.cdMode = ChangeDetectorStatus.Checked;

这意味着下次更改检测周期开始时,将不会对子视图执行任何更改检测。

如何运行 更改此类视图的检测选项很少。首先是将子视图的 cdMode 更改为 ChangeDetectorStatus.CheckOnce,这可以使用 ngDoCheck 生命周期挂钩中的 this._changeRef.markForCheck() 来完成:

      constructor(private _changeRef: ChangeDetectorRef) {   }
    
      ngDoCheck() {
        this._changeRef.markForCheck();
      }

这只是将当前视图及其父视图的 cdMode 更改为 ChangeDetectorStatus.CheckOnce,因此下次执行更改检测时会检查当前视图。

查看完整示例 here in the sources,但这里是要点:

          constructor(ref: ChangeDetectorRef) {
            setInterval(() => {
              this.numberOfTicks ++
              // the following is required, otherwise the view will not be updated
              this.ref.markForCheck();
              ^^^^^^^^^^^^^^^^^^^^^^^^
            }, 1000);
          }

第二个选项是在视图本身上调用 detectChanges,如果 cdMode 不是 ChangeDetectorStatus.CheckedChangeDetectorStatus.Errored,它将在当前视图上 run change detection。由于 onPush angular 将 cdMode 设置为 ChangeDetectorStatus.CheckOnce,angular 将 运行 更改检测。

所以 ngDoCheck 不会覆盖已更改的检测,它只是在每个已更改的检测周期调用它,它唯一的工作是将当前视图 cdMode 设置为 checkOnce,以便在下一个更改检测周期中,它会检查更改。有关详细信息,请参阅 。如果当前视图的变化检测模式是checkAlways(如果不使用onPush策略默认设置),ngDoCheck似乎没有用。

简单来说:

It's generally a component check on following occasions:

  • Update child component input bindings
  • Update DOM interpolations
  • Update query list

使用:

Deep Watch changes which angular misses out.

注:

angular 用于更改检测的默认算法通过比较输入绑定属性值来查找差异,理解。凉爽的。

ngOnChanges() 的限制

由于 angular 更改检测的默认行为,ngOnChanges 无法检测是否有人更改了对象的 属性 或将项目推入数组。 所以 ngDoCheck 来回避了。

ngDoCheck() 哇!

检测深层变化,例如 属性 对象或项目的变化被推入数组,即使没有引用变化。惊人的权利