翻译说明: 中英文对照, 意译。
本文出自 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
会在 ngOnChanges
和 ngOnInit
之后触发, 但是当这个钩子触发的时候,组件是不是正被检测呢?
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 中的变更检测的一切 讲述了 和组件变更检测相关的三个核心操作:
update child component input bindings
更新子组件的输入绑定update DOM interpolations
更新DOM插值update query list
更新查询列表
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
.
父组件App
将 o
对象向下传递给子组件 a-comp
In 2 seconds it mutates the object by updating the name
and id
properties:
2 秒后, 通过更新 name
和 id
属性改变了对象 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 ngDoCheck
lifecycle 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()
"