美文网首页我爱编程
Everything you need to know abou

Everything you need to know abou

作者: CK110 | 来源:发表于2017-12-27 17:00 被阅读254次

    原文链接

    探索底层的实现和用例

    如果像我一样想要对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应该更新,则会引发异常。

    相关文章

      网友评论

        本文标题:Everything you need to know abou

        本文链接:https://www.haomeiwen.com/subject/szwugxtx.html