[Angular][translate]有关Angular的变动检测

原文连接: https://blog.angularindepth.com/everything-you-need-to-know-about-change-detection-in-angular-8006c51d206fcss

这篇文章只是我本身一边看原文一边随手翻译的,个中错误以及表述多有不正确,阅读原文才是最佳选择。node

若是你也像我同样想对Angular的变动检测有更深刻的了解,去看源码无疑是必须的选择,由于网络上相关的信息实在是太少了。大多数文章都只是指出每一个组件都有本身的变动检测器,可是并无继续深刻,他们大多关注于不可变对象和变动检测策略的使用。因此这篇文章的目的是告诉你,为何使用不可变对象会有效?变动检测策略是如何影响到检测的?同时,这篇文章也能让你能对不一样场景下提出相应的性能优化方法。git

这篇文章由两部分组成,第一部分技术性较强且有不少源码部分的引用。主要解释了变动检测在底层的工做细节。基于Angular-4.0.1。须要注意的是,2.4.1以后版本的变动检测策略有较大的变化。若是你对之前版本的变动检测有兴趣,能够阅读这篇回答github

第二部分主要讲解了如何在应用中使用变动检测,这部分对于Angular2+都是相同的。由于Angular的公共API并无发生变化。typescript

核心概念-视图View

Angular的文档中通篇都提到了一个Angular应用是一个组件树。可是Angular底层其实使用了一个低级抽象-视图View。视图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.app

视图中元素的属性能够发生变化,可是视图中元素的数量和顺序不能变化。若是想要改变的话,须要经过VireContainerRef来执行插入,移动和删除操做。每一个视图都会包括多个View Container。异步

在这篇文章中,组件和组件视图的概念是互相可替代的。

须要注意的是:网络上不少文章都把咱们这里所描述的视图做为了变动检测对象或者ChangeDetectorRef。事实上,Angular中并无一个单独的对象用来作变动检测,全部的变动检测都在视图上直接运行。

export interface ViewData {
  def: ViewDefinition;
  root: RootData;
  renderer: Renderer2;
  // index of component provider / anchor.
  parentNodeDef: NodeDef|null;
  parent: ViewData|null;
  viewContainerParent: ViewData|null;
  component: any;
  context: any;
  // Attention: Never loop over this, as this will
  // create a polymorphic usage site.
  // Instead: Always loop over ViewDefinition.nodes,
  // and call the right accessor (e.g. `elementData`) based on
  // the NodeType.
  nodes: {[key: number]: NodeData};
  state: ViewState;
  oldValues: any[];
  disposables: DisposableFn[]|null;
}
复制代码

视图的状态

每一个视图都有本身的状态,基于这些状态的值,Angular会决定是否对这个视图和他全部的子视图运行变动检测。视图有不少状态值,可是在这篇文章中,下面四个状态值最为重要:

// Bitmask of states
export const enum ViewState {
  FirstCheck = 1 << 0,
  ChecksEnabled = 1 << 1,
  Errored = 1 << 2,
  Destroyed = 1 << 3
}
复制代码

若是CheckedEnabled值为false或者视图处于Errored或者Destroyed状态时,这个视图的变动检测就不会执行。默认状况下,全部视图初始化时都会带上CheckEnabled,除非使用了ChangeDetectionStrategy.onPush。有关onPush咱们稍后再讲。这些状态也能够被合并使用,好比一个视图能够同时有FirstCheck和CheckEnabled两个成员。

针对操做视图,Angular中有一些封装出的高级概念,详见这里。一个概念是ViewRef。他的_view属性囊括了组件视图,同时它还有一个方法detectChanges。当一个异步事件触发时,Angular从他的最顶层的ViewRef开始触发变动检测,而后对子视图继续进行变动检测。

ChangeDectionRef能够被注入到组件的构造函数中。这个类的定义以下:

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 {
    /** * Destroys the view and all of the data structures associated with it. */
    abstract destroy(): void;
    abstract get destroyed(): boolean;
    abstract onDestroy(callback: Function): any
}
复制代码

变动检测操做

负责对视图运行变动检测的主要逻辑属于checkAndUpdateView方法。他的大部分功能都是对子组件视图进行操做。从宿主组件开始,这个方法被递归调用做用于每个组件。这意味着当递归树展开时,在下一次调用这个方法时子组件会成为父组件。

当在某个特定视图上开始触发这个方法时,如下操做会依次发生:

  1. 若是这是视图的第一次检测,将ViewState.firstCheck设置为true,不然为false;
  2. 检查并更新子组件/指令的输入属性-checkAndUpdateDirectiveInline
  3. 更新子视图的变动检测状态(属于变动检测策略实现的一部分)
  4. 对内嵌视图运行变动检测(重复列表中的步骤)
  5. 若是绑定的值发生变化,调用子组件的onChanges生命周期钩子;
  6. 调用子组件的OnInit和DoCheck两个生命周期钩子(OnInit只在第一次变动检测时调用)
  7. 在子组件视图上更新ContentChildren列表-checkAndUpdateQuery
  8. 调用子组件的AfterContentInit和AfterContentChecked(前者只在第一次检测时调用)-callProviderLifecycles
  9. 若是当前视图组件上的属性发生变化,更新DOM
  10. 对子视图执行变动检测-callViewAction
  11. 更新当前视图组件的ViewChildren列表-checkAndUpdateQuery
  12. 调用子组件的AfterViewInit和AfterViewChecked-callProviderLifecycles
  13. 对当前视图禁用检测

在以上操做中有几点须要注意

深刻这些操做的含义

假设咱们如今有一棵组件树:

在上面的讲解中咱们得知了每一个组件都和一个组件视图相关联。每一个视图都使用ViewState.checksEnabled初始化了。这意味着当Angular开始变动检测时,整棵组件树上的全部组件都会被检测;

假设此时咱们须要禁用AComponent和它的子组件的变动检测,咱们只要将它的ViewState.checksEnabled设置为false就行。这听起来很容易,可是改变state的值是一个很底层的操做,所以Angular在视图上提供了不少方法。经过ChangeDetectorRef每一个组件能够得到与之关联的视图。

class ChangeDetectorRef {
  markForCheck() : void
  detach() : void
  reattach() : void
  
  detectChanges() : void
  checkNoChanges() : void
}
复制代码

detach

这个方法简单的禁止了对当前视图的检测;

detach(): void {
    this._view.state &= ~ViewState.checksEnabled;
}
复制代码

在组件中的使用方法:

export class AComponent {
    constructor( private cd: ChangeDectectorRef, ) {
        this.cd.detach();
    }
}
复制代码

这样就会致使在接下来的变动检测中AComponent及子组件都会被跳过。

这里有两点须要注意:

  • 虽然咱们只修改了AComponent的state值,可是他的子组件也不会被执行变动检测;
  • 因为AComponent及其子组件不会有变动检测,所以他们的DOM也不会有任何更新

下面是一个简单示例,点击按钮后在输入框中修改就不再会引发下面的p标签的变化,外部父组件传递进来的值发生变化也不会触发变动检测:

import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
@Component({
    selector: 'app-change-dection',
    template: ` <input [(ngModel)]="name"> <button (click)="stopCheck()">中止检测</button> <p>{{name}}</p> `,
    styleUrls: ['./change-dection.component.css']
})
export class ChangeDectionComponent implements OnInit {
    name = 'erik';
    constructor( private cd: ChangeDetectorRef, ) { }
    ngOnInit() {
    }
    stopCheck() {
        this.cd.detach();
    }
}
复制代码

reattach

文章第一部分提到:若是AComponent的输入属性aProp发生变化,OnChanges生命周期钩子仍会被调用,这意味着一旦咱们得知输入属性发生变化,咱们能够激活当前组件的变动检测并在下一个tick中继续detach变动检测。

reattach(): void { 
    this._view.state |= ViewState.ChecksEnabled; 
}
复制代码
export class ChangeDectionComponent implements OnInit, OnChanges {
    @Input() aProp: string;
    name = 'erik';
    constructor( private cd: ChangeDetectorRef, ) { }
    ngOnInit() {
    }
    ngOnChanges(change) {
        this.cd.reattach();
        setTimeout(() => {
            this.cd.detach();
        });
    }
}
复制代码

上面这种作法几乎与将ChangeDetectionStrategy改成OnPush是等效的。他们都在第一轮变动检测后禁用了检测,当父组件向子组件传值发生变化时激活变动检测,而后又禁用变动检测。

须要注意的是,在这种状况下,只有被禁用检测分支最顶层组件的OnChanges钩子才会被触发,并非这个分支的全部组件的OnChanges都会被触发,缘由也很简单,被禁用检测的这个分支内不存在了变动检测,天然内部也不会向子元素变动所传递的值,可是顶层的元素仍能够接受到外部变动的输入属性。

译注:其实将retach()和detach()放在ngOnChanges()和OnPush策略仍是不同的,OnPush策略的确是只有在input值的引用发生变化时才出发变动检测,这一点是正确的,可是OnPush策略自己并不影响组件内部的值的变化引发的变动检测,而上例中组件内部的变动检测也会被禁用。若是将这段逻辑放在ngDoCheck()中才更正确一点。

maskForCheck

上面的reattach()方法能够对当前组件开启变动检测,然而若是这个组件的父组件或者更上层的组件的变动检测仍被禁用,用reattach()后是没有任何做用的。这意味着reattach()方法只对被禁用检测分支的最顶层组件有意义。

所以咱们须要一个方法,能够将当前元素及全部祖先元素直到根元素的变动检测都开启。ChangeDetectorRef提供了markForCheck方法:

let currView: ViewData|null = view;
while (currView) {
  if (currView.def.flags & ViewFlags.OnPush) {
    currView.state |= ViewState.ChecksEnabled;
  }
  currView = currView.viewContainerParent || currView.parent;
}
复制代码

在这个实现中,它简单的向上迭代并启用对全部直到根组件的祖先组件的检查。

这个方法在何时有用呢?禁用变动检测策略以后,ngDoCheck生命周期仍是会像ngOnChanges同样被触发。固然,跟OnChanges同样,DoCheck也只会在禁用检测分支的顶部组件上被调用。可是咱们就能够利用这个生命周期钩子来实现本身的业务逻辑和将这个组件标记为能够进行一轮变动检测。

因为Angular只检测对象引用,咱们须要经过对对象的某些属性来进行这种脏检查:

// 这里若是外部items变化为改变引用位置,此组件是不会执行变动检测的
// 可是若是在DoCheck()钩子中调用markForCheck
// 因为OnPush策略不影响DoCheck的执行,这样就能够侦测到这个变动
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

Angular提供了一个方法detectChanges,对当前组件和全部子组件运行一轮变动检测。这个方法会无视组件的ViewState,也就是说这个方法不会改变组件的变动检测策略,组件仍会维持原有的会被检测或不会被检测状态。

export class AComponent {
  @Input() inputAProp;

  constructor(public cd: ChangeDetectorRef) {
    this.cd.detach();
  }

  ngOnChanges(values) {
    this.cd.detectChanges();
  }
}
复制代码

经过这个方法咱们能够实现一个相似Angular.js的手动调用脏检查。

checkNoChanges

这个方法是用来当前变动检测没有产生任何变化。他执行了文章第一部分1,7,8三个操做,并在发现有变动致使DOM须要更新时抛出异常。

结束!哈!

相关文章
相关标签/搜索