Angular 2 Directive Lifecycle

在介紹 Angular 2 Directive Lifecycle (生命週期) 以前,咱們先來介紹一下 Angular 2 中 Directive (指令) 與 Component (組件) 的關係。typescript

圖片描述

咱們再來看一下 Angular 2 中定義的指令和組件接口:segmentfault

// angular2/packages/core/src/metadata/directives.ts

export interface Directive {
   selector?: string;  // 用於定義組件在HTML代碼中匹配的標籤
   inputs?: string[];  // 指令的輸入屬性
   outputs?: string[];  // 指令的輸出屬性
   host?: {[key: string]: string};  // 綁定宿主的屬性、事件等
   providers?: Provider[];  // 設置指令及其子指令能夠用的服務
   exportAs?: string;  // 導出指令,使得能夠在模板中調用
   queries?: {[key: string]: any};  // 設置指令的查詢條件
}

export interface Component extends Directive {
   changeDetection?: ChangeDetectionStrategy;  // 指定組件使用的變化檢測策略
   viewProviders?: Provider[];     // 設置組件及其子組件(不含ContentChildren)能夠用的服務
   moduleId?: string;  // 包含該組件模塊的 id,它被用於解析 模版和樣式的相對路徑
   templateUrl?: string;  // 爲組件指定一個外部模板的URL地址
   template?: string;  // 爲組件指定一個內聯的模板
   styleUrls?: string[];  // 爲組件指定一系列用於該組件的樣式表文件
   styles?: string[];  // 爲組件指定內聯樣式
   animations?: any[];  // 設置組件相關動畫
   encapsulation?: ViewEncapsulation;  // 設置組件視圖包裝選項
   interpolation?: [string, string];  // 設置默認的插值運算符,默認是"{{"和"}}"
   entryComponents?: Array<Type<any>|any[]>;  // 設置須要被提早編譯的組件
}

經過觀察上圖與 Angular 2 中指令與組件的接口定義,咱們能夠總結出指令與組件之間的關係:組件繼承於指令,並擴展了與 UI 視圖相關的屬性,如 template、styles、animations、encapsulation 等。瀏覽器

下面咱們進入正題,開始介紹 Angular 2 指令的生命週期,它是用來記錄指令從建立、應用及銷燬的過程。Angular 2 提供了一系列與指令生命週期相關的鉤子,便於咱們監控指令生命週期的變化,並執行相關的操做。Angular 2 中全部的鉤子以下圖所示:angular2

圖片描述

怎麼那麼多鉤子,是否是被嚇到了,沒事咱們基於指令與組件的區別來分個類:app

  • 指令與組件共有的鉤子ide

    • ngOnChanges函數

    • ngOnInit動畫

    • ngDoCheckthis

    • ngOnDestroyspa

  • 組件特有的鉤子

    • ngAfterContentInit

    • ngAfterContentChecked

    • ngAfterViewInit

    • ngAfterViewChecked

Angular 2 指令生命週期鉤子的做用及調用順序

  1. ngOnChanges - 當數據綁定輸入屬性的值發生變化時調用

  2. ngOnInit - 在第一次 ngOnChanges 後調用

  3. ngDoCheck - 自定義的方法,用於檢測和處理值的改變

  4. ngAfterContentInit - 在組件內容初始化以後調用

  5. ngAfterContentChecked - 組件每次檢查內容時調用

  6. ngAfterViewInit - 組件相應的視圖初始化以後調用

  7. ngAfterViewChecked - 組件每次檢查視圖時調用

  8. ngOnDestroy - 指令銷燬前調用

Angular 2 指令生命週期鉤子詳解

在詳細介紹指令生命週期鉤子以前,咱們先來介紹一下構造函數:

constructor

組件的構造函數會在全部的生命週期鉤子以前被調用,它主要用於依賴注入或執行簡單的數據初始化操做。

import { Component, ElementRef } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h1>Welcome to Angular World</h1>
    <p>Hello {{name}}</p>
  `,
})
export class AppComponent {
  name: string = '';

  constructor(public elementRef: ElementRef) { // 使用構造注入的方式注入依賴對象
    this.name = 'Semlinker'; // 執行初始化操做
  }
}

ngOnChanges

當數據綁定輸入屬性的值發生變化的時候,Angular 將會主動調用 ngOnChanges 方法。它會得到一個 SimpleChanges 對象,包含綁定屬性的新值和舊值,它主要用於監測組件輸入屬性的變化。

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h4>Welcome to Angular World</h4>
    <exe-child name="exe-child-component"></exe-child>
  `,
})
export class AppComponent { }

child.component.ts

import { Component, Input, SimpleChanges, OnChanges } from '@angular/core';

@Component({
    selector: 'exe-child',
    template: `
      <p>Child Component</p>  
      <p>{{ name }}</p>
    `
})
export class ChildComponent implements OnChanges{
    @Input()
    name: string;

    ngOnChanges(changes: SimpleChanges) {
        console.dir(changes);
    }
}

以上代碼運行後,瀏覽器的輸出結果:

圖片描述

ngOnInit

在第一次 ngOnChanges 執行以後調用,而且只被調用一次。它主要用於執行組件的其它初始化操做或獲取組件輸入的屬性值。

import { Component, Input, OnInit } from '@angular/core';

@Component({
    selector: 'exe-child',
    template: `
     <p>父組件的名稱:{{pname}} </p>
    `
})
export class ChildComponent implements OnInit {
    @Input()
    pname: string; // 父組件的名稱

    constructor() {
        console.log('ChildComponent constructor', this.pname); // Output:undefined
    }

    ngOnInit() {
        console.log('ChildComponent ngOnInit', this.pname); // output: 輸入的pname值
    }
}

ngOnDestory

在指令被銷燬前,將會調用 ngOnDestory 方法。它主要用於執行一些清理操做,好比:移除事件監聽、清除定時器、退訂 Observable 等。

@Directive({
    selector: '[destroyDirective]'
})
export class OnDestroyDirective implements OnDestroy {
  sayHello: number;
  
  constructor() {
    this.sayHiya = window.setInterval(() => console.log('hello'), 1000);
  }
  
  ngOnDestroy() {
     window.clearInterval(this.sayHiya);
  }
}

ngDoCheck

當組件的輸入屬性發生變化時,將會觸發 ngDoCheck 方法。咱們可使用該方法,自定義咱們的檢測邏輯。它也能夠用來加速咱們變化檢測的速度。

ngAfterContentInit

在組件使用 ng-content 指令的狀況下,Angular 會在將外部內容放到視圖後用。它主要用於獲取經過 @ContentChild 或 @ContentChildren 屬性裝飾器查詢的內容視圖元素。

具體使用示例,請參考 - Angular 2 ContentChild & ContentChildren

ngAfterContentChecked

在組件使用 ng-content 指令的狀況下,Angular 會在檢測到外部內容的綁定或者每次變化的時候調用。

ngAfterViewInit

在組件相應的視圖初始化以後調用,它主要用於獲取經過 @ViewChild 或 @ViewChildren 屬性裝飾器查詢的視圖元素。

具體使用示例,請參考 - Angular 2 ViewChild & ViewChildren

ngAfterViewChecked

組件每次檢查視圖時調用

Angular 2 LifecycleHooks 、SimpleChanges 等相關接口

LifecycleHooks 接口

export interface OnChanges { ngOnChanges(changes: SimpleChanges): void; }

export interface OnInit { ngOnInit(): void; }

export interface DoCheck { ngDoCheck(): void; }

export interface OnDestroy { ngOnDestroy(): void; }

export interface AfterContentInit { ngAfterContentInit(): void; }

export interface AfterContentChecked { ngAfterContentChecked(): void; }

export interface AfterViewInit { ngAfterViewInit(): void; }

export interface AfterViewChecked { ngAfterViewChecked(): void; }

SimpleChange

// 用於表示變化對象
export class SimpleChange {
  constructor(public previousValue: any, 
      public currentValue: any, 
      public firstChange: boolean) {}
      
  // 標識是否爲首次變化
  isFirstChange(): boolean { return this.firstChange; }
}

SimpleChanges

export interface SimpleChanges { [propName: string]: SimpleChange; }

Angular 2 View 詳解

在 Angular 2 中 View (視圖) 由三個部分組成:

  • Elements - 元素

  • Bindings - 綁定

  • Events - 事件

Angular 2 TemplateRef & ViewContainerRef 這篇文章中,咱們介紹了 Angular 2 支持的 View(視圖) 類型:

  • Embedded Views - Template 模板元素

  • Host Views - Component 組件

接下來咱們來分析一下組件對應的 Host Views,具體示例以下:

child.component.ts

import { Component, Input, SimpleChanges, OnChanges, AfterViewChecked } from '@angular/core';

@Component({
    selector: 'exe-child',
    template: `
      <p>Child Component</p>  
      <p>{{ name }}</p>
    `
})
export class ChildComponent implements OnChanges, AfterViewChecked{
    @Input()
    name: string;

    ngOnChanges(changes: SimpleChanges) {
        console.dir(changes);
        setTimeout(() => {
            this.name = 'exe-child-component-1'
        }, 0);
    }

    ngAfterViewChecked() {
        console.log('ngAfterViewChecked hook has been called');
    }
}

app.component.ts

import { Component } from '@angular/core';

@Component({
  selector: 'my-app',
  template: `
    <h4>Welcome to Angular World</h4>
    <exe-child name="exe-child-component"></exe-child>
  `,
})
export class AppComponent { }

以上代碼運行後,瀏覽器的輸出結果:

圖片描述

接下來咱們來分析一下 ChildComponent 組件,先來看一下編譯後的 component.ngfactory.js 文件。

ChildComponent/component.ngfactory.js 代碼片斷:

function View_ChildComponent0(viewUtils,parentView,parentIndex,parentElement) {
  var self = this;
  ...
  self._expr_7 = jit_CD_INIT_VALUE5;
}

/*
* 用於初始化模板內的元素
* ChildComponent - template
* <p>Child Component</p>  
* <p>{{ name }}</p>
*/
View_ChildComponent0.prototype.createInternal = function(rootSelector) {
  var self = this;
  var parentRenderNode = self.renderer.createViewRoot(self.parentElement);
  ...
  
  // (1) 建立p元素 - <p>Child Component</p>  
  self._el_1 = jit_createRenderElement6(self.renderer,parentRenderNode,
      'p', jit__object_Object_7,self.debug(1,1,6));
      
  // 建立文本元素,設置內容爲 - 'Child Component'
  self._text_2 = self.renderer.createText(self._el_1,'Child Component',
      self.debug(2,1,9));
  
  // (2) 建立p元素 - <p>{{ name }}</p>
  self._el_4 = jit_createRenderElement6(self.renderer,parentRenderNode,
       'p', jit__object_Object_7,self.debug(4,2,6));
       
  self._text_5 = self.renderer.createText(self._el_4,'',self.debug(5,2,9));
  
  self.init(null,(self.renderer.directRenderer? null: [...]
  ),null);
  return null;
};

// 執行變化檢測                                                       
View_ChildComponent0.prototype.detectChangesInternal = function(throwOnChange) {
  var self = this;
  self.debug(5,2,9);
  var currVal_7 = jit_inlineInterpolate8(1,'',self.context.name,'');
  if (jit_checkBinding9(throwOnChange,self._expr_7,currVal_7)) {
    self.renderer.setText(self._text_5,currVal_7);
    self._expr_7 = currVal_7;
  }
};

ChildComponent/wrapper.ngfactory.js 代碼片斷:

function Wrapper_ChildComponent() {
  var self = this;
  self._changed = false;
  self._changes = {}; // 建立Changes對象
  self.context = new jit_ChildComponent0();
  self._expr_0 = jit_CD_INIT_VALUE1; // {}
}

Wrapper_ChildComponent.prototype.ngOnDestroy = function() { };

Wrapper_ChildComponent.prototype.check_name =function(currValue,
  throwOnChange, forceUpdate) {
  var self = this;
  // 判斷值是否更新,jit_checkBinding2中直接使用looseIdentical(oldValue, newValue)
  // 進行全等比較(===)
  if ((forceUpdate || jit_checkBinding2(throwOnChange,self._expr_0,currValue))) {
    self._changed = true;
    self.context.name = currValue;
    // 建立name關聯的SimpleChange對象
    self._changes['name'] = new jit_SimpleChange3(self._expr_0,currValue);
    self._expr_0 = currValue;
  }
};


Wrapper_ChildComponent.prototype.ngDoCheck = function(view,el,throwOnChange) {
  var self = this;
  var changed = self._changed;
  self._changed = false;
  if (!throwOnChange) { if (changed) {
    self.context.ngOnChanges(self._changes);
    jit_setBindingDebugInfoForChanges4(view.renderer,el,self._changes);
    self._changes = {};
  } }
  return changed;
};
 ...
return Wrapper_ChildComponent
})

我有話說

1.註冊指令生命週期鉤子時,必定要實現對應的接口麼 ?

註冊指令生命週期鉤子時,實現對應的接口不是必須的,接口能夠幫助咱們在開發階段儘早地發現錯誤,由於咱們有可能在註冊生命週期鉤子的時候,寫錯了某個鉤子的名稱,在運行時可能不會拋出任何異常,但頁面顯示卻不是預期的效果,所以建議讀者仍是遵照該開發規範。另外還要注意的一點是,TypeScript 中定義的接口,是不會編譯生成 ES5 相關代碼,它只用於編譯階段作校驗。

未完待續

相關文章
相關標籤/搜索