探索底层的实现和用例
如果像我一样想要对Angular中的变化检测机制有一个全面的了解,您将不得不去探索源代码,因为网络上没有太多可用的信息。大多数文章提到,每个组件都有自己的change detector
,负责检查组件,但是他们不会超出这个范围,主要关注immutables
和 change detection strategy
。本文为您提供了理解为什么使用immutables
的用例以及变更change detection strategy
如何影响变化检测。另外,您将通过从本文中学到的知识自行创建各种性能优化方案。
本文由两部分组成。第一部分是技术性的,包含很多链接到源代码。它详细解释了变化检测机制是如何工作的。其内容基于最新的Angular版本 - 4.0.1。在这个版本中如何实现变更检测机制的方式与早期的2.4.1不同。如果感兴趣,你可以阅读一些关于它如何工作的解答stackoverflow。
第二部分显示了如何在应用程序中使用change detection
,其内容适用于较早的2.4.1和最新的4.0.1版本的Angular,因为公共API没有改变。
View as a core concept
在教程中已经提到,Angular应用程序是一个组件树。然而,在Angular下使用一个称为view的底层抽象。视图和组件之间有直接的关系 - 一个视图与一个组件相关联,而另一个视图与另一个组件相关联。视图的component
属性件属性是组件类实例的引用。属性检查和DOM更新的所有操作都是在视图上执行的,因此,认为Angular是视图树时,技术上更为正确,组件可以被描述为视图的更高层概念。以下是您可以在源码中查看有关视图的信息:
A View is a fundamental building block of the application UI. It is the smallest grouping of Elements which are created and destroyed together.
Properties of elements in a View can change, but the structure (number and order) of elements in a View cannot. Changing the structure of Elements can only be done by inserting, moving or removing nested Views via a ViewContainerRef. Each View can contain many View Containers.
在这篇文章中,我将会交替地使用 component view(组件视图)和 component (组件)的概念。
It’s important to note here that all articles on the web and answers on StackOverflow regarding change detection refer to the View I’m describing here as Change Detector Object or ChangeDetectorRef. In reality, there’s no separate object for change detection and View is what change detection runs on.
每个view
都有一个nodes属性来关联子组件视图,因此可以对子视图执行操作。
View state
每个view
都有一个state,它非常重要,因为Angular根据它的值来判断是否为当前view
和它的子组件执行change detection
。有许多可能的状态,有以下几点:
- FirstCheck
- ChecksEnabled
- Errored
- Destroyed
如果ChecksEnabled
值为false或者当前view
处于Errored
或者Destroyed
状态,view
和它的child views
将不会执行Change detection
。默认情况下,除非使用ChangeDetectionStrategy.OnPush
,否则所有view
都使用ChecksEnabled
进行初始化。而且,state
可以组合,例如,view
可以同时设置FirstCheck``和ChecksEnabled
。
Angular有一些高级概念来操纵view
。我在这里写了一些关于它们的文章。其中一个概念就是ViewRef。它封装了底层的组件视图,并有一个方法detectChanges。发生异步事件时,Angular会在其最顶端的ViewRef上触发change detection,在自身运行change detection
之后,它会为其子视图执行change detection
。
这个viewRef
是你可以使用ChangeDetectorRef
在组件构造函数中注入的:
export class AppComponent {
constructor(cd: ChangeDetectorRef) { ... }
从这个类的定义可以看出:
export declare abstract class ChangeDetectorRef {
abstract checkNoChanges(): void;
abstract detach(): void;
abstract detectChanges(): void;
abstract markForCheck(): void;
abstract reattach(): void;
}
export abstract class ViewRef extends ChangeDetectorRef {
...
}
Change detection operations
checkAndUpdateView函数中是负责运行视图change detection
的主逻辑。其大部分功能都在子组件视图上执行操作。从host
组件开始,为每个组件递归地调用该函数。这意味着在递归树展开时,子组件在下一次调用时变成父组件。
当一个特定的视图触发这个函数,它按照指定的顺序执行以下操作:
- sets
ViewState.firstCheck
totrue
if a view is checked for the first time and tofalse
if it was already checked before - checks and updates input properties on a child component/directive instance
- updates child view change detection state (part of change detection strategy implementation)
- runs change detection for the embedded views (repeats the steps in the list)
-
calls
OnChanges
lifecycle hook on a child component if bindings changed -
calls
OnInit
andngDoCheck
on a child component (OnInit
is called only during first check) -
updates
ContentChildren
query list on a child view component instance -
calls
AfterContentInit
andAfterContentChecked
lifecycle hooks on child component instance (AfterContentInit
is called only during first check) -
updates DOM interpolations for the
current view
if properties oncurrent view
component instance changed - runs change detection for a child view (repeats the steps in this list)
-
updates
ViewChildren
query list on the current view component instance -
calls
AfterViewInit
andAfterViewChecked
lifecycle hooks on child component instance (AfterViewInit
is called only during first check) - disables checks for the current view (part of change detection strategy implementation)
根据上面列出的操作,有几件事情需要强调。
首先,onChange
s生命周期钩子在子视图被检查之前发在子组件上被触发,即使跳过子视图的变化检测,它也会被触发。这是重要的信息,我们将在文章的第二部分看到如何利用这些知识。
第二件事是视图的DOM更新作为更改检测机制的一部分,发生在检查视图后。这意味着如果一个组件没有被脏检测,即使模板中使用的组件属性发生改变,DOM也不会被更新。模板在第一次检查之前被渲染。我所说的DOM更新实际上是插值更新。因此,如果<span>some {{name}}</span>
,则DOM元素span
将在第一次检查之前呈现。在检查过程中,只会重新渲染{{name}}
部分。
另一个有趣的观察,在change detection
期间可以改变子组件视图的state
。我之前提到,默认情况下,所有组件视图都使用ChecksEnabled
进行初始化,但是对于所有使用OnPush
策略更改检测的组件,在第一次检查(列表中的操作9)后都被禁用:
if (view.def.flags & ViewFlags.OnPush) {
view.state &= ~ViewState.ChecksEnabled;
}
这意味着在接下来的更改检测运行期间,该组件视图及其所有子组件将跳过该检查。有关OnPush策略的文档指出,只有在组件的绑定发生了变化时,组件才会被检查。所以要做到这一点,必须通过设置ChecksEnabled位来启用检查。这是下面的代码(操作2)所做的事:
if (compView.def.flags & ViewFlags.OnPush) {
compView.state |= ViewState.ChecksEnabled;
}
仅当父视图绑定发生更改并且子组件视图已使用ChangeDetectionStrategy.OnPush初始化时,状态才会更新。最后,当前视图的change detection
负责启动子视图的change detection
(操作8)。这是检查子组件视图状态的位置,如果是ChecksEnabled,则对此视图执行更改检测。这是相关的代码:
viewState = view.state;
...
case ViewAction.CheckAndUpdate:
if ((viewState & ViewState.ChecksEnabled) &&
(viewState & (ViewState.Errored | ViewState.Destroyed)) === 0) {
checkAndUpdateView(view);
}
}
现在我们知道了view
中的state
控制了是否需要对当前view和它的子组件view执行change detection
。我们可以控制这个state
吗?事实证明,我们可以,这是本文的第二部分。
一些生命周期钩子在DOM更新之前(3,4,5)和之后的一些(9)被调用。所以如果你有下面的组件层次结构:A - > B - > C,这里是钩子调用和绑定更新的顺序:
A: AfterContentInit
A: AfterContentChecked
A: Update bindings
B: AfterContentInit
B: AfterContentChecked
B: Update bindings
C: AfterContentInit
C: AfterContentChecked
C: Update bindings
C: AfterViewInit
C: AfterViewChecked
B: AfterViewInit
B: AfterViewChecked
A: AfterViewInit
A: AfterViewChecked
Exploring the implications
假设我们有以下组件树:
如上所述,每个component
关联一个component view
。每个view
都根据ViewState.ChecksEnabled
进行初始化,这意味着当Angular运行change detection
时,将检查树中的每个组件。
假设我们要禁用AComponent
及其chilren
的change detection
。这很容易做 - 我们只需要将ViewState.ChecksEnabled
设置为false
即可。更改状态是一个底层的操作,所以Angular为我们提供了一些view
上可用的公共方法。每个组件都可以通过ChangeDetectorRef
获取相关view
。对于这个类Angular文档定义了以下公共接口:
class ChangeDetectorRef {
markForCheck() : void
detach() : void
reattach() : void
detectChanges() : void
checkNoChanges() : void
}
detach
允许我们操纵state
的第一种方法是detach
(分离),它只是简单地禁止检查当前视图。
detach(): void { this._view.state &= ~ViewState.ChecksEnabled; }
我们来看看如何在代码中使用它:
export class AComponent {
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
可以确保在以后的change detection
运行期间,以AComponent
开始的左分支将被跳过(橙色组件将不会被检查):
有两件事要注意 - 首先是即使我们只是改变了AComponent的state
,它的所有子组件也不会被检查。其次,由于不会对左分支组件执行change detection
,因此其模板中的DOM也不会更新。这里是一个小例子来演示它:
@Component({
selector: 'a-comp',
template: `<span>See if I change: {{changed}}</span>`
})
export class AComponent {
constructor(public cd: ChangeDetectorRef) {
this.changed = 'false';
setTimeout(() => {
this.cd.detach();
this.changed = 'true';
}, 2000);
}
组件第一次被检查时,span
中的文本渲染为See if I change: false
。2秒后,changed
属性的值变为true
,但是span
中的内容不会改变。如果去除this.cd.detach()
,则一切正常。
Detach
与onPush
等效,设置onPush
相当于在构造函数中使用this.cd.detach
。他们都改变视图状态并禁用检查。如果输入绑定更改,OnPush会将状态更改为启用检查。但是在detach
中,你必须在ngOnChanges中自己打开检查。
reattach
如文章的第一部分所示,如果在AppComponent
上的绑定aProp发生更改,则触发AComponent上的OnChanges生命周期挂钩。这意味着,一旦我们被通知输入属性发生变化,我们可以激活当前组件的change detector
(变化检测器)来运行change detection
,并在下一个tick
将其detach
(分离)。
export class AComponent {
@Input() inputAProp;
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
ngOnChanges(values) {
this.cd.reattach();
setTimeout(() => {
this.cd.detach();
})
}
由于reattach
只是简单的setViewState.ChecksEnabled
。
reattach(): void { this._view.state |= ViewState.ChecksEnabled; }
这几乎等于ChangeDetectionStrategy设置为OnPush时执行的操作:在第一次更改检测运行后禁用检查,在父组件绑定属性更改时启用它,运行后禁用。
markForCheck
reattach
方法只能检查当前组件,但是如果其父组件的changed detection
未启用,则不起作用。这意味着reattach
方法仅对禁用分支中的最顶层组件有用。
我们需要一种方法来启用所有父组件的检查。可以使用markForCheck
方法:
let currView: ViewData|null = view;
while (currView) {
if (currView.def.flags & ViewFlags.OnPush) {
currView.state |= ViewState.ChecksEnabled;
}
currView = currView.viewContainerParent || currView.parent;
}
从实现中可以看到,它只是向上迭代,启用对每个父组件的检查直到根组件。
什么时候这有用?和ngOnChanges一样,即使组件使用OnPush策略,也会触发ngDoCheck生命周期钩子。同样,它仅在禁用分支中的最顶层组件中触发,而不是在禁用分支中的每个组件触发。但是我们可以使用这个钩子来执行自定义的逻辑,并标记我们的组件运行一个change detection
循环。由于Angular只检查对象引用,所以我们可以实现一些对象属性的脏检查:
Component({
...,
changeDetection: ChangeDetectionStrategy.OnPush
})
MyComponent {
@Input() items;
prevLength;
constructor(cd: ChangeDetectorRef) {}
ngOnInit() {
this.prevLength = this.items.length;
}
ngDoCheck() {
if (this.items.length !== this.prevLength) {
this.cd.markForCheck();
this.prevLenght = this.items.length;
}
}
detectChanges
有一种方法可以对当前组件及其所有子项运行一次change detection
。使用detectChanges
方法。此方法运行当前组件视图的变更检测,而不管其状态如何,这意味着当前视图的检查可能保持禁用状态,并且在下面的定期更改检测运行期间将不检查组件。
export class AComponent {
@Input() inputAProp;
constructor(public cd: ChangeDetectorRef) {
this.cd.detach();
}
ngOnChanges(values) {
this.cd.detectChanges();
}
输入属性改变时,DOM也会随之更新,尽管change detector
是detached
状态。
checkNoChanges
change detector
(变化检测器)上可用的最后一种方法,可确保在当前的change detection
运行中不会发生变化。基本上,它执行列表中1,7,8个操作,如果发现一个更改的绑定或者确定DOM应该更新,则会引发异常。