If you think `ngDoCheck` means your component is being checked — read this article (如果你认为 `ngDoCheck`意味着组件正被检测, 那么阅读这篇文章)

翻译说明: 中英文对照, 意译。
本文出自 AngularInDepth
原文传送门


I use OnPush, why is ngDoCheck triggered?
我使用了 OnPush, 为什么 ngDoCheck 会被触发?

There’s one question that comes up again and again on stackoverflow.
有个问题在 stackoverflow 上反复出现。

The question is about ngDoCheck lifecycle hook that is triggered for a component that implements OnPush change detection strategy.
这个问题和 ngDoCheck 生命周期钩子有关 —— 在实现了 OnPush 变更检测策略的组件上触发了 ngDoCheck

It’s usually formulated something like:
这个问题通常是这样的:


I have used OnPush strategy for my component and no bindings have
changed, but the ngDoCheck lifecycle hook is still triggered. Is the strategy
not working?
(我已经在我的组件里使用了 OnPush 策略, 而且绑定没有改变, 但是 ngDoCheck 生命周期钩子仍然触发了。难道这个策略没起作用?)


It’s an interesting question which stems from misunderstanding of
when the the ngDoCheck lifecycle hook is triggered and why it’s
provided by the framework.
这是个很有趣的问题,源于对 ngDoCheck 生命周期钩子的触发时机以及框为何提供这个钩子的误解。

This article gives an answer to this common question by showing when the hook in question is triggered and what’s its purpose.
本文通过演示问题中的钩子何时触发以及它的目的来回答这个常见问题。

When is ngDoCheck triggered (ngDoCheck 何时触发)

The official docs don’t tell much about this lifecycle hook:
官方文档对于这个生命周期钩子并没有讲述很多内容:


Detect and act upon changes that Angular can’t or won’t detect on its own.
Called during every change detection run, immediately after
ngOnChanges() and ngOnInit().
(检测并应对那些 Angular 自己不能够或者不会检测的变更,在每次变更检测过程中紧跟在 ngOnChanges()ngOnInit()之后调用)


So we know that it’s triggered after ngOnChanges and ngOnInit but is the component being checked or not when it’s triggered?
由此, 我们知道了 ngDoCheck 会在 ngOnChangesngOnInit 之后触发, 但是当这个钩子触发的时候,组件是不是正被检测呢?

To answer that question, we need to first define “component check”.
要回答这个问题, 我们需要首先定义 "组件检测"。

The article Everything you need to know about change detection in Angular states that there are three core operations related to component change detection:
文章 你需要了解的 Angular 中的变更检测的一切 讲述了 和组件变更检测相关的三个核心操作:

Besides these core operations Angular triggers lifecycle hooks as part of change detection.
除了这些核心操作以外, Angular 还会触发生命周期钩子, 而这也是变更检测的一部分。

What is interesting is that the hooks for the child component are triggered when the parent component is being checked.
有趣的是,当检查父组件时,子组件的钩子会被触发。

Here is the small example to demonstrate that. Suppose we have the following components tree:
这里有一个小例子来说明这一点。假设我们有以下组件树:

ComponentA
    ComponentB
        ComponentC

So when Angular runs change detection the order of operations is the following:
当 Angular 进行变更检测时, 操作的顺序如下:

Checking A component:
检测 A 组件

  - update B input bindings
    更新 B 组件的 输入绑定

  - call NgDoCheck on the B component
    调用 B 组件的 ngDoCheck

  - update DOM interpolations for component A
     更新 A 组件 的 DOM 插值

 Checking B component:
  检测 B 组件:

    - update C input bindings
      更新 C 组件的输入绑定

    - call NgDoCheck on the C component
      调用 C 组件的 ngDoCheck

    - update DOM interpolations for component B
     更新 B 组件 的 DOM 插值

   Checking C component:
    检测 C 组件

      - update DOM interpolations for component C
       更新 C 组件的 DOM 插值

If you read the article on change detection I referenced above you will notice that it’s a bit abridged list of operations, but it will do for the purpose of demonstrating when ngDoCheck is triggered.
如果你阅读了上面提到的有关变更检测的文章,你就会注意到这是一个有点简化的操作列表,但是这仅是为了演示 ngDoCheck 的触发时机。

You can see that ngDoCheck is called on the child component when the parent component is being checked.
你能够看到 当正在检测父组件时, 调用了子组件的 ngDoCheck

Now suppose we implement onPush strategy for the B component. How does the flow change? Let’s see:
现在, 假设我们让 B 组件实现 onPush 策略。 这个流程会怎样改变, 我们来看一下:

Checking A component:
  检测 A 组件

  - update B input bindings
    更新 B 组件的输入绑定

  - call NgDoCheck on the B component
    调用 B 组件的 ngDoCheck

  - update DOM interpolations for component A
    更新 A 组件的 DOM 插值

 if (bindings changed) -> checking B component:
  如果 绑定改变了 -> 检测 B 组件

    - update C input bindings
      更新 C 组件的 输入绑定

    - call NgDoCheck on the C component
      调用 C 组件的 ngDoCheck

    - update DOM interpolations for component B
      更新 B 组件的 DOM 插值
 
   Checking C component:
    检测 C 组件
      - update DOM interpolations for component C
        更新 C 组件的 DOM 插值

So with the introduction of the OnPush strategy we see the small condition if (bindings changed) -> checking B component is added before B component is checked.
引入 OnPush 策略后,我们看到了个小条件,“如果(绑定改变了)—>检查B组件”, 添加到了检查 B 组件之前。

If this condition doesn’t hold, you can see that Angular won’t execute the operations under checking B component.
如果这个条件不满足, 你会看到 Angular 不会执行检测 B 组件下面的操作。

However, the NgDoCheck on the B component is still triggered even though the B component will not be checked.
然而, 即使 B 组件没有被检测 , B 组件的 ngDoCheck 仍然触发了 。

It’s important to understand that the hook is triggered only for the top level B component with OnPush strategy, and not triggered for its children — C component in our case.
重要的是要理解,ngDoCheck 钩子只会在 实现 OnPush 策略的顶级 B 组件触发, 而不会在它的子组件(我们的案例中是 C 组件)触发。

So, the answer to the question:


I have used OnPush strategy for my component, but the ngDoCheck lifecycle hook is still triggered. Is the strategy not working?


is — the strategy is working. The hook is triggered by design and the next chapter shows why.
因此, 对于问题 "我已经在我的组件中使用了OnPush策略, 但是ngDoCheck生命周期钩子仍然触发了。难道这个策略没起作用?" 的回答是: 这个策略在起作用。 ngDoCheck 钩子按照设计触发了, 下一章会解释原因。

Why do we need ngDoCheck? (我们为什么需要 ngDoCheck?)

You probably know that Angular tracks binding inputs by object reference.
你可能知道 Angular 通过 对象引用跟踪输入绑定。

It means that if an object reference hasn’t changed the binding change is not detected and change detection is not executed for a component that uses OnPush strategy.
这意味着,对于使用了 OnPush 策略的组件来说, 如果对象引用并没有改变, 绑定变更就不会检测到, 那么组件的变更检测就不会执行。

In AngularJS this is the standard approach as well and in the following example the changes to o object won’t be detected:
在 AngularJS中,这也是标准的方法,在下面的例子中,将不会检测到 o 对象的改变:

const o = {some: 3};

$scope.$watch(
  () => {  return o;},
  () => {  console.log('changed'); } // nothing is logged
);

$timeout(() => {  o.some = 4; }, 2000);

But AngularJS has a few variations of standard $watch function to allow tracking object and array mutations — deep watch and collection watch.
但是 AngularJS 有几个 $watch 函数的变体, 可以追踪对象和数组的改变——深度监视和集合监视。

To enabled the deep watch you have to pass the third parameter true to the $watch function:
要开启深度监视, 你必须传入第三个参数 true

$scope.$watch(
  () => {  return o;},
  () => {  console.log('changed'); }, // logs `changed`
  true
);

And to watch collections you have to use $watchCollection method:
要监视集合的话, 你必须使用 $watchCollection 方法:

const o = [3];

$scope.$watchCollection(
  () => {  return o;},
  () => {  console.log('changed'); } // logs `changed`
);

$timeout(() => {  o.push(4) }, 2000);

But there’s no equivalent in Angular.
但是, 在 Angular 中并没有等价物。

If we want to track an object or an array mutations we need to manually do that.
如果要跟踪一个对象或数组的改变,我们需要手动去做。

And if discover the change we need to let Angular know so that it will run change detection for a component even though the object reference hasn't changed.
并且如果发现了改变,我们需要让 Angular 知道, 这样它就会对组件执行变更检测,即使对象引用并没有发生改变。

Let’s use the example from AngularJS in the Angular application.
我们在 Angular 应用中使用 AngularJS 中的例子

We have A component that uses OnPush change detection strategy and takes o object through input binding.
我们创建使用 OnPush 变更检测策略的 A 组件,该组件有个输入绑定对象 o

Inside the component template it references the name property:
在组件模板中,它引用了name属性:

@Component({
  selector: 'a-comp',
  template: `<h2>The name is: {{o.name}}</h2>`,
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class AComponent {
  @Input() o;
}

And we also have the parent App component that passes the o object down to the child a-comp.
父组件Appo 对象向下传递给子组件 a-comp

In 2 seconds it mutates the object by updating the name and id properties:
2 秒后, 通过更新 nameid 属性改变了对象 o

@Component({
  selector: 'my-app',
  template: `
    <h1>Hello {{name}}</h1>
    <a-comp [o]="o"></a-comp>
  `,
})
export class App {
  name = `Angular! v${VERSION.full}`;
  o = {id: 1, name: 'John'};

  ngOnInit() {
    setTimeout(() => {
      this.o.id = 2;
      this.o.name = 'Jane';
    }, 2000);
  }
}

Since Angular tracks object reference and we mutate the object without changing the reference Angular won’t pick up the changes and it will not run change detection for the A component.
由于 Angular 追踪的是对象引用, 我们在不改变引用的情况下改变了对象, Angular 不会获取到改变,也就不会对 A组件执行变更检测。

Thus the new name property value will not be re-rendered in DOM.
因此, 新的 name 属性值不会在 DOM 中重新渲染。

Luckily, we can use the ngDoCheck lifecycle hook to check for object mutation and notify Angular using markForCheck method.
幸运的是, 我们可以使用 ngDoCheck 生命周期钩子检测对象改变, 然后通过使用 markForCheck 方法通知 Angular。

In the implementation above we will track only id property mutation but if required you can implement full-blown change tracking similar to deep watch in AngularJS.
上面的实现中, 我们只追踪了 id 属性的改变, 但是如果有需求的话, 你可以实现类似于 AngularJS 中深度监视的成熟的变更追踪。

Let’s do just it:
我们做一下:

export class AComponent {
  @Input() o;

  // store previous value of `id`
  id;

  constructor(private cd: ChangeDetectorRef) {}

  ngOnChanges() {
    // every time the object changes 
    // store the new `id`
    this.id = this.o.id;
  }

  ngDoCheck() {
    // check for object mutation
    if (this.id !== this.o.id) {
      this.cd.markForCheck();
    }
  }
}

Here is the plunker that shows that approach.
这是该方法的 plunker 演示。

To practice on your own you can try to implement the full-blown deep watch and watch collection approaches similar to AngularJS.
你可以尝试实现类似于 AngularJS 的深度监视和监视集合方法。

One thing to bear in mind is that Angular team recommends using immutable objects instead of object mutations so that you can rely on default Angular bindings change tracking mechanism.
要记住的一点是,Angular 团队建议使用不可变对象替代对象改变,这样你就可以依赖于默认的 Angular 绑定变更跟踪机制。

But since it’s not always possible as you’ve just learnt Angular provides a fallback in the form of ngDoChecklifecycle hook.
但是,使用不可变对象并不总是可能的。正如你刚刚所学的,Angular 提供了ngDoCheck生命周期钩子形式的回退。


原文评论里有部分非常重要的信息, 这里做一下总结, 对原文以及评论感兴趣的读者,可以去原文品鉴。

在使用了 OnPush 变更检测策略的组件中,只有当输入绑定改变时才会进行变更检测。如果发生改变的属性不是输入绑定, 那么 Angular 就不会对这个组件执行变更检测。

Since ngDoCheck is triggered on the child component before the child component is checked, if you call markForCheck() the component will be checked during current change detection cycle once Angular starts running change detection for the child component.
由于ngDoCheck 是在子组件触发, 并且是在子组件变更检测之前触发。 如果你调用了 markForCheck() , 一旦 Angular 对子组件开始执行变更检测, 该子组件会在当前变更检测循环期间检测。

The hook ngDoCheck is called when Angular already runs change detection, so markForCheck simply marks current component and all its parent for check.
生命周期钩子 ngDoCheck 调用时, Angular 已经执行变更检测。
因此, markForCheck 只是标记要对当前组件及其所有父组件进行检查。

何时用 markForCheck? 何时用 detectChanges
应当使用 markForCheck的情况:
1、已经在变更检测过程里,比如在 ngDoCheck 生命周期钩子里
2、你希望在不久的将来执行一次变更检测循环
如果在这两种情况下使用了 detectChanges, 会造成一次多余的变更检测

ngDoCheck可以这样理解:
Angular: "我要对这个组件进行检测, 但是如果这个组件的变更检测策略是 OnPush, 并且它的输入绑定没有改变, 我就会跳过它及其所有子组件。如果你仍然想让我对这个组件进行变更检测, 那么调用 cd.markForCheck()"

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容