Everything you need to know about change detection in Angular

原文链接

探索底层的实现和用例

如果像我一样想要对Angular中的变化检测机制有一个全面的了解,您将不得不去探索源代码,因为网络上没有太多可用的信息。大多数文章提到,每个组件都有自己的change detector,负责检查组件,但是他们不会超出这个范围,主要关注immutableschange 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。有许多可能的状态,有以下几点:

  1. FirstCheck
  2. ChecksEnabled
  3. Errored
  4. 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组件开始,为每个组件递归地调用该函数。这意味着在递归树展开时,子组件在下一次调用时变成父组件。

当一个特定的视图触发这个函数,它按照指定的顺序执行以下操作:

  1. sets ViewState.firstCheck to true if a view is checked for the first time and to false if it was already checked before
  2. checks and updates input properties on a child component/directive instance
  3. updates child view change detection state (part of change detection strategy implementation)
  4. runs change detection for the embedded views (repeats the steps in the list)
  5. calls OnChanges lifecycle hook on a child component if bindings changed
  6. calls OnInit and ngDoCheck on a child component (OnInit is called only during first check)
  7. updates ContentChildren query list on a child view component instance
  8. calls AfterContentInit and AfterContentChecked lifecycle hooks on child component instance (AfterContentInit is called only during first check)
  9. updates DOM interpolations for the current view if properties on current view component instance changed
  10. runs change detection for a child view (repeats the steps in this list)
  11. updates ViewChildren query list on the current view component instance
  12. calls AfterViewInit and AfterViewChecked lifecycle hooks on child component instance (AfterViewInit is called only during first check)
  13. disables checks for the current view (part of change detection strategy implementation)

根据上面列出的操作,有几件事情需要强调。

首先,onChanges生命周期钩子在子视图被检查之前发在子组件上被触发,即使跳过子视图的变化检测,它也会被触发。这是重要的信息,我们将在文章的第二部分看到如何利用这些知识。

第二件事是视图的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

假设我们有以下组件树:

1_aRo_mATLsi0B3p7E6Ndv4Q.png

如上所述,每个component关联一个component view。每个view都根据ViewState.ChecksEnabled进行初始化,这意味着当Angular运行change detection时,将检查树中的每个组件。

假设我们要禁用AComponent及其chilrenchange 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开始的左分支将被跳过(橙色组件将不会被检查):

1_QtTCrT0cVGxoPJAapKGSAA.png

有两件事要注意 - 首先是即使我们只是改变了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(),则一切正常。

DetachonPush等效,设置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 detectordetached状态。

checkNoChanges

change detector(变化检测器)上可用的最后一种方法,可确保在当前的change detection运行中不会发生变化。基本上,它执行列表中1,7,8个操作,如果发现一个更改的绑定或者确定DOM应该更新,则会引发异常。

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

推荐阅读更多精彩内容