Angular 組件生命周期

2022-06-28 13:42 更新

生命周期鉤子

當(dāng) Angular 實例化組件類并渲染組件視圖及其子視圖時,組件實例的生命周期就開始了。生命周期一直伴隨著變更檢測,Angular 會檢查數(shù)據(jù)綁定屬性何時發(fā)生變化,并按需更新視圖和組件實例。當(dāng) Angular 銷毀組件實例并從 DOM 中移除它渲染的模板時,生命周期就結(jié)束了。當(dāng) Angular 在執(zhí)行過程中創(chuàng)建、更新和銷毀實例時,指令就有了類似的生命周期。

你的應(yīng)用可以使用生命周期鉤子方法來觸發(fā)組件或指令生命周期中的關(guān)鍵事件,以初始化新實例,需要時啟動變更檢測,在變更檢測過程中響應(yīng)更新,并在刪除實例之前進行清理。

先決條件

在使用生命周期鉤子之前,你應(yīng)該對這些內(nèi)容有一個基本的了解:

  • TypeScript
  • Angular 應(yīng)用設(shè)計基礎(chǔ)

響應(yīng)生命周期事件

可以通過實現(xiàn)一個或多個 Angular ?core ?庫中定義的生命周期鉤子接口來響應(yīng)組件或指令生命周期中的事件。這些鉤子讓你有機會在適當(dāng)?shù)臅r候?qū)M件或指令實例進行操作,比如 Angular 創(chuàng)建、更新或銷毀這個實例時。

每個接口都有唯一的一個鉤子方法,它們的名字是由接口名再加上 ?ng ?前綴構(gòu)成的。比如,?OnInit ?接口的鉤子方法叫做 ?ngOnInit()?。如果你在組件或指令類中實現(xiàn)了這個方法,Angular 就會在首次檢查完組件或指令的輸入屬性后,緊接著調(diào)用它。

@Directive({selector: '[appPeekABoo]'})
export class PeekABooDirective implements OnInit {
  constructor(private logger: LoggerService) { }

  // implement OnInit's `ngOnInit` method
  ngOnInit() {
    this.logIt('OnInit');
  }

  logIt(msg: string) {
    this.logger.log(`#${nextId++} ${msg}`);
  }
}

你不必實現(xiàn)所有生命周期鉤子,只要實現(xiàn)你需要的那些就可以了。

生命周期的順序

當(dāng)你的應(yīng)用通過調(diào)用構(gòu)造函數(shù)來實例化一個組件或指令時,Angular 就會調(diào)用那個在該實例生命周期的適當(dāng)位置實現(xiàn)了的那些鉤子方法。

Angular 會按以下順序執(zhí)行鉤子方法??梢杂盟鼇韴?zhí)行以下類型的操作。

鉤子方法

用途

時機

ngOnChanges()

當(dāng) Angular 設(shè)置或重新設(shè)置數(shù)據(jù)綁定的輸入屬性時響應(yīng)。 該方法接受當(dāng)前和上一屬性值的 SimpleChanges 對象

注意,這發(fā)生的非常頻繁,所以你在這里執(zhí)行的任何操作都會顯著影響性能。

如果組件綁定過輸入屬性,那么在 ngOnInit() 之前以及所綁定的一個或多個輸入屬性的值發(fā)生變化時都會調(diào)用。

注意,如果你的組件沒有輸入屬性,或者你使用它時沒有提供任何輸入屬性,那么框架就不會調(diào)用 ngOnChanges()。

ngOnInit()

在 Angular 第一次顯示數(shù)據(jù)綁定和設(shè)置指令/組件的輸入屬性之后,初始化指令/組件。

在第一輪 ngOnChanges() 完成之后調(diào)用,只調(diào)用一次。而且即使沒有調(diào)用過 ngOnChanges(),也仍然會調(diào)用 ngOnInit()(比如當(dāng)模板中沒有綁定任何輸入屬性時)。

ngDoCheck()

檢測,并在發(fā)生 Angular 無法或不愿意自己檢測的變化時作出反應(yīng)。 

緊跟在每次執(zhí)行變更檢測時的 ngOnChanges() 和 首次執(zhí)行變更檢測時的 ngOnInit() 后調(diào)用。

ngAfterContentInit()

當(dāng) Angular 把外部內(nèi)容投影進組件視圖或指令所在的視圖之后調(diào)用。

第一次 ngDoCheck() 之后調(diào)用,只調(diào)用一次。

ngAfterContentChecked()

每當(dāng) Angular 檢查完被投影到組件或指令中的內(nèi)容之后調(diào)用。

ngAfterContentInit() 和每次 ngDoCheck() 之后調(diào)用

ngAfterViewInit()

當(dāng) Angular 初始化完組件視圖及其子視圖或包含該指令的視圖之后調(diào)用。

第一次 ngAfterContentChecked() 之后調(diào)用,只調(diào)用一次。

ngAfterViewChecked()

每當(dāng) Angular 做完組件視圖和子視圖或包含該指令的視圖的變更檢測之后調(diào)用。

ngAfterViewInit() 和每次 ngAfterContentChecked() 之后調(diào)用。

ngOnDestroy()

每當(dāng) Angular 每次銷毀指令/組件之前調(diào)用并清掃。 在這兒反訂閱可觀察對象和分離事件處理器,以防內(nèi)存泄漏。

在 Angular 銷毀指令或組件之前立即調(diào)用。

生命周期范例

現(xiàn)場演練 / 下載范例通過在受控于根組件 ?AppComponent ?的一些組件上進行的一系列練習(xí),演示了生命周期鉤子的運作方式。 每一個例子中,組件都扮演了組件測試臺的角色,以展示出一個或多個生命周期鉤子方法。

下表列出了這些練習(xí)及其簡介。 范例代碼也用來闡明后續(xù)各節(jié)的一些特定任務(wù)。

組件

說明

?Peek-a-boo?

展示每個生命周期鉤子,每個鉤子方法都會在屏幕上顯示一條日志。

?Spy?

展示了如何在自定義指令中使用生命周期鉤子。 SpyDirective 實現(xiàn)了 ngOnInit() 和 ngOnDestroy() 鉤子,并且使用它們來觀察和匯報一個元素何時進入或離開當(dāng)前視圖。

?OnChanges?

演示了每當(dāng)組件的輸入屬性之一發(fā)生變化時,Angular 如何調(diào)用 ngOnChanges() 鉤子。并且演示了如何解釋傳給鉤子方法的 changes 對象。

?DoCheck?

實現(xiàn)了一個 ngDoCheck() 方法,通過它可以自定義變更檢測邏輯。 監(jiān)視該鉤子把哪些變更記錄到了日志中,觀察 Angular 以什么頻度調(diào)用這個鉤子。

?AfterView?

顯示 Angular 中的視圖所指的是什么。 演示了 ngAfterViewInit() 和 ngAfterViewChecked() 鉤子。

?AfterContent?

展示如何把外部內(nèi)容投影進組件中,以及如何區(qū)分“投影進來的內(nèi)容”和“組件的子視圖”。 演示了 ngAfterContentInit() 和 ngAfterContentChecked() 鉤子。

?Counter?

演示了一個組件和一個指令的組合,它們各自有自己的鉤子。

初始化組件或指令

使用 ?ngOnInit()? 方法執(zhí)行以下初始化任務(wù)。

  • 在構(gòu)造函數(shù)外部執(zhí)行復(fù)雜的初始化。組件的構(gòu)造應(yīng)該既便宜又安全。比如,你不應(yīng)該在組件構(gòu)造函數(shù)中獲取數(shù)據(jù)。當(dāng)在測試中創(chuàng)建組件時或者決定顯示它之前,你不應(yīng)該擔(dān)心新組件會嘗試聯(lián)系遠程服務(wù)器。
  • ?ngOnInit()? 是組件獲取初始數(shù)據(jù)的好地方。

  • 在 Angular 設(shè)置好輸入屬性之后設(shè)置組件。構(gòu)造函數(shù)應(yīng)該只把初始局部變量設(shè)置為簡單的值。
  • 請記住,只有在構(gòu)造完成之后才會設(shè)置指令的數(shù)據(jù)綁定輸入屬性。如果要根據(jù)這些屬性對指令進行初始化,請在運行 ?ngOnInit()? 時設(shè)置它們。

?ngOnChanges()? 方法是你能訪問這些屬性的第一次機會。Angular 會在調(diào)用 ?ngOnInit()? 之前調(diào)用 ?ngOnChanges()?,而且之后還會調(diào)用多次。但它只調(diào)用一次 ?ngOnInit()?。

在實例銷毀時進行清理

把清理邏輯放進 ?ngOnDestroy()? 中,這個邏輯就必然會在 Angular 銷毀該指令之前運行。

這里是釋放資源的地方,這些資源不會自動被垃圾回收。如果你不這樣做,就存在內(nèi)存泄漏的風(fēng)險。

  • 取消訂閱可觀察對象和 DOM 事件。
  • 停止 interval 計時器。
  • 反注冊該指令在全局或應(yīng)用服務(wù)中注冊過的所有回調(diào)。

?ngOnDestroy()? 方法也可以用來通知應(yīng)用程序的其它部分,該組件即將消失。

一般性例子

下面的例子展示了各個生命周期事件的調(diào)用順序和相對頻率,以及如何在組件和指令中單獨使用或同時使用這些鉤子。

所有生命周期事件的順序和頻率

為了展示 Angular 如何以預(yù)期的順序調(diào)用鉤子,?PeekABooComponent ?演示了一個組件中的所有鉤子。

實際上,你很少會(幾乎永遠不會)像這個演示中一樣實現(xiàn)所有這些接口。

下列快照反映了用戶單擊 Create... 按鈕,然后單擊 Destroy... 按鈕后的日志狀態(tài)。


日志信息的日志和所規(guī)定的鉤子調(diào)用順序是一致的: ?OnChanges?、?OnInit?、?DoCheck ?(3x)、?AfterContentInit?、?AfterContentChecked ?(3x)、 ?AfterViewInit?、?AfterViewChecked ?(3x)和 ?OnDestroy?

注意,該日志確認了在創(chuàng)建期間那些輸入屬性(這里是 ?name ?屬性)沒有被賦值。 這些輸入屬性要等到 ?onInit()? 中才可用,以便做進一步的初始化。

如果用戶點擊Update Hero按鈕,就會看到另一個 ?OnChanges ?和至少兩組 ?DoCheck?、?AfterContentChecked ?和 ?AfterViewChecked ?鉤子。 注意,這三種鉤子被觸發(fā)了很多次,所以讓它們的邏輯盡可能保持精簡是非常重要的!

使用指令來監(jiān)視 DOM

這個 ?Spy ?例子演示了如何在指令和組件中使用鉤子方法。?SpyDirective ?實現(xiàn)了兩個鉤子 ?ngOnInit()? 和 ?ngOnDestroy()?,以便發(fā)現(xiàn)被監(jiān)視的元素什么時候位于當(dāng)前視圖中。

這個模板將 ?SpyDirective ?應(yīng)用到由父組件 ?SpyComponent ?管理的 ?ngFor ?內(nèi)的 ?<div>? 中。

該例子不執(zhí)行任何初始化或清理工作。它只是通過記錄指令本身的實例化時間和銷毀時間來跟蹤元素在視圖中的出現(xiàn)和消失。

像這樣的間諜指令可以深入了解你無法直接修改的 DOM 對象。你無法觸及內(nèi)置 ?<div>? 的實現(xiàn),也無法修改第三方組件,但是可以用指令來監(jiān)視這些元素。

這個指令定義了 ?ngOnInit()? 和 ?ngOnDestroy()? 鉤子,它通過一個注入進來的 ?LoggerService ?把消息記錄到父組件中去。

let nextId = 1;

// Spy on any element to which it is applied.
// Usage: <div appSpy>...</div>
@Directive({selector: '[appSpy]'})
export class SpyDirective implements OnInit, OnDestroy {
  private id = nextId++;

  constructor(private logger: LoggerService) { }

  ngOnInit() {
    this.logger.log(`Spy #${this.id} onInit`);
  }

  ngOnDestroy() {
    this.logger.log(`Spy #${this.id} onDestroy`);
  }
}

你可以把這個偵探指令寫到任何內(nèi)置元素或組件元素上,以觀察它何時被初始化和銷毀。 下面是把它附加到用來重復(fù)顯示英雄數(shù)據(jù)的這個 ?<div>? 上。

<p *ngFor="let hero of heroes" appSpy>
  {{hero}}
</p>

每個“偵探”的創(chuàng)建和銷毀都可以標出英雄所在的那個 ?<div>? 的出現(xiàn)和消失。 添加一個英雄就會產(chǎn)生一個新的英雄 ?<div>?。偵探的 ?ngOnInit()? 記錄下了這個事件。

Reset 按鈕清除了這個 ?heroes ?列表。 Angular 從 DOM 中移除了所有英雄的 div,并且同時銷毀了附加在這些 div 上的偵探指令。 偵探的 ?ngOnDestroy()? 方法匯報了它自己的臨終時刻。

同時使用組件和指令的鉤子

在這個例子中,?CounterComponent ?使用了 ?ngOnChanges()? 方法,以便在每次父組件遞增其輸入屬性 ?counter ?時記錄一次變更。

這個例子將前例中的 ?SpyDirective ?用于 ?CounterComponent ?的日志,以便監(jiān)視這些日志條目的創(chuàng)建和銷毀。

使用變更檢測鉤子

一旦檢測到該組件或指令的輸入屬性發(fā)生了變化,Angular 就會調(diào)用它的 ?ngOnChanges()? 方法。 這個 onChanges 范例通過監(jiān)控 ?OnChanges()? 鉤子演示了這一點。

ngOnChanges(changes: SimpleChanges) {
  for (const propName in changes) {
    const chng = changes[propName];
    const cur  = JSON.stringify(chng.currentValue);
    const prev = JSON.stringify(chng.previousValue);
    this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
  }
}

?ngOnChanges()? 方法獲取了一個對象,它把每個發(fā)生變化的屬性名都映射到了一個?SimpleChange?對象, 該對象中有屬性的當(dāng)前值和前一個值。這個鉤子會在這些發(fā)生了變化的屬性上進行迭代,并記錄它們。

這個例子中的 ?OnChangesComponent ?組件有兩個輸入屬性:?hero ?和 ?power?。

@Input() hero!: Hero;
@Input() power = '';

宿主 ?OnChangesParentComponent ?綁定了它們,就像這樣:

<on-changes [hero]="hero" [power]="power"></on-changes>

下面是此例子中的當(dāng)用戶做出更改時的操作演示:


日志條目把 power 屬性的變化顯示為字符串。但請注意,?ngOnChanges()? 方法不會捕獲對 ?hero.name? 更改。這是因為只有當(dāng)輸入屬性的值發(fā)生變化時,Angular 才會調(diào)用該鉤子。在這種情況下,?hero ?是輸入屬性,?hero ?屬性的值是對 hero 對象引用 。當(dāng)它自己的 ?name ?屬性的值發(fā)生變化時,對象引用并沒有改變。

響應(yīng)視圖的變更

當(dāng) Angular 在變更檢測期間遍歷視圖樹時,需要確保子組件中的某個變更不會嘗試更改其父組件中的屬性。因為單向數(shù)據(jù)流的工作原理就是這樣的,這樣的更改將無法正常渲染。

如果你需要做一個與預(yù)期數(shù)據(jù)流反方向的修改,就必須觸發(fā)一個新的變更檢測周期,以允許渲染這種變更。這些例子說明了如何安全地做出這些改變。

AfterView 例子展示了 ?AfterViewInit()? 和 ?AfterViewChecked()? 鉤子,Angular 會在每次創(chuàng)建了組件的子視圖后調(diào)用它們。

下面是一個子視圖,它用來把英雄的名字顯示在一個 ?<input>? 中:

@Component({
  selector: 'app-child-view',
  template: `
    <label for="hero-name">Hero name: </label>
    <input type="text" id="hero-name" [(ngModel)]="hero">
  `
})
export class ChildViewComponent {
  hero = 'Magneta';
}

?AfterViewComponent ?把這個子視圖顯示在它的模板中

template: `
  <div>child view begins</div>
    <app-child-view></app-child-view>
  <div>child view ends</div>
`

下列鉤子基于子視圖中的每一次數(shù)據(jù)變更采取行動,它只能通過帶?@ViewChild?裝飾器的屬性來訪問子視圖。

export class AfterViewComponent implements  AfterViewChecked, AfterViewInit {
  private prevHero = '';

  // Query for a VIEW child of type `ChildViewComponent`
  @ViewChild(ChildViewComponent) viewChild!: ChildViewComponent;

  ngAfterViewInit() {
    // viewChild is set after the view has been initialized
    this.logIt('AfterViewInit');
    this.doSomething();
  }

  ngAfterViewChecked() {
    // viewChild is updated after the view has been checked
    if (this.prevHero === this.viewChild.hero) {
      this.logIt('AfterViewChecked (no change)');
    } else {
      this.prevHero = this.viewChild.hero;
      this.logIt('AfterViewChecked');
      this.doSomething();
    }
  }
  // ...
}

在更新視圖之前等待

在這個例子中,當(dāng)英雄名字超過 10 個字符時,?doSomething()? 方法會更新屏幕,但在更新 ?comment ?之前會等一個節(jié)拍(tick)。

// This surrogate for real business logic sets the `comment`
private doSomething() {
  const c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
  if (c !== this.comment) {
    // Wait a tick because the component's view has already been checked
    this.logger.tick_then(() => this.comment = c);
  }
}

在組件的視圖合成完之后,就會觸發(fā) ?AfterViewInit()? 和 ?AfterViewChecked()? 鉤子。如果你修改了這段代碼,讓這個鉤子立即修改該組件的數(shù)據(jù)綁定屬性 ?comment?,你就會發(fā)現(xiàn) Angular 拋出一個錯誤。

?LoggerService.tick_then()? 語句把日志的更新工作推遲了一個瀏覽器 JavaScript 周期,也就觸發(fā)了一個新的變更檢測周期。

編寫精簡的鉤子方法來避免性能問題

當(dāng)你運行 AfterView 范例時,請注意當(dāng)沒有發(fā)生任何需要注意的變化時,Angular 仍然會頻繁的調(diào)用 ?AfterViewChecked()?。 要非常小心你放到這些方法中的邏輯或計算量。


響應(yīng)被投影內(nèi)容的變更

內(nèi)容投影是從組件外部導(dǎo)入 HTML 內(nèi)容,并把它插入在組件模板中指定位置上的一種途徑。 可以在目標中通過查找下列結(jié)構(gòu)來認出內(nèi)容投影。

  • 元素標簽中間的 HTML。
  • 組件模板中的 ?<ng-content>? 標簽。

AngularJS 的開發(fā)者把這種技術(shù)叫做 ?transclusion?。

這個 AfterContent 例子探索了 ?AfterContentInit()? 和 ?AfterContentChecked()? 鉤子。Angular 會在把外部內(nèi)容投影進該組件時調(diào)用它們。

這次不再通過模板來把子視圖包含進來,而是改為從 ?AfterContentComponent ?的父組件中導(dǎo)入它。下面是父組件的模板:

`<after-content>
  <app-child></app-child>
</after-content>`

注意,?<app-child>? 標簽被包含在 ?<after-content>? 標簽中。 永遠不要在組件標簽的內(nèi)部放任何內(nèi)容 —— 除非你想把這些內(nèi)容投影進這個組件中。

現(xiàn)在來看該組件的模板:

template: `
  <div>projected content begins</div>
    <ng-content></ng-content>
  <div>projected content ends</div>
`

?<ng-content>? 標簽是外來內(nèi)容的占位符。 它告訴 Angular 在哪里插入這些外來內(nèi)容。 在這里,被投影進去的內(nèi)容就是來自父組件的 ?<app-child>? 標簽。


使用 AfterContent 鉤子

AfterContent 鉤子和 AfterView 相似。關(guān)鍵的不同點是子組件的類型不同。

  • AfterView 鉤子所關(guān)心的是 ?ViewChildren?,這些子組件的元素標簽會出現(xiàn)在該組件的模板里面
  • AfterContent 鉤子所關(guān)心的是 ?ContentChildren?,這些子組件被 Angular 投影進該組件中。

下列 AfterContent 鉤子基于子級內(nèi)容中值的變化而采取相應(yīng)的行動,它只能通過帶有?@ContentChild?裝飾器的屬性來查詢到“子級內(nèi)容”。

export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
  private prevHero = '';
  comment = '';

  // Query for a CONTENT child of type `ChildComponent`
  @ContentChild(ChildComponent) contentChild!: ChildComponent;

  ngAfterContentInit() {
    // contentChild is set after the content has been initialized
    this.logIt('AfterContentInit');
    this.doSomething();
  }

  ngAfterContentChecked() {
    // contentChild is updated after the content has been checked
    if (this.prevHero === this.contentChild.hero) {
      this.logIt('AfterContentChecked (no change)');
    } else {
      this.prevHero = this.contentChild.hero;
      this.logIt('AfterContentChecked');
      this.doSomething();
    }
  }
  // ...
}
不需要等待內(nèi)容更新
該組件的 ?doSomething()? 方法會立即更新該組件的數(shù)據(jù)綁定屬性 ?comment?。而無需延遲更新以確保正確渲染 。
Angular 在調(diào)用 AfterView 鉤子之前,就已調(diào)用完所有的 AfterContent 鉤子。 在完成該組件視圖的合成之前, Angular 就已經(jīng)完成了所投影內(nèi)容的合成工作。 ?AfterContent...? 和 ?AfterView...? 鉤子之間有一個小的時間窗,允許你修改宿主視圖。

自定義變更檢測邏輯

要監(jiān)控 ?ngOnChanges()? 無法捕獲的變更,你可以實現(xiàn)自己的變更檢查邏輯,比如 DoCheck 的例子。這個例子展示了你如何使用 ?ngDoCheck()? 鉤子來檢測和處理 Angular 自己沒有捕捉到的變化。

DoCheck 范例使用下面的 ?ngDoCheck()? 鉤子擴展了 OnChanges 范例:

ngDoCheck() {

  if (this.hero.name !== this.oldHeroName) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
    this.oldHeroName = this.hero.name;
  }

  if (this.power !== this.oldPower) {
    this.changeDetected = true;
    this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
    this.oldPower = this.power;
  }

  if (this.changeDetected) {
      this.noChangeCount = 0;
  } else {
      // log that hook was called when there was no relevant change.
      const count = this.noChangeCount += 1;
      const noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
      if (count === 1) {
        // add new "no change" message
        this.changeLog.push(noChangeMsg);
      } else {
        // update last "no change" message
        this.changeLog[this.changeLog.length - 1] = noChangeMsg;
      }
  }

  this.changeDetected = false;
}

這段代碼會檢查某些感興趣的值,捕獲并把它們當(dāng)前的狀態(tài)和之前的進行比較。當(dāng) ?hero ?或 ?power ?沒有實質(zhì)性變化時,它就會在日志中寫一條特殊的信息,這樣你就能看到 ?DoCheck()? 被調(diào)用的頻率。其結(jié)果很有啟發(fā)性。


雖然 ?ngDoCheck()? 鉤子可以檢測出英雄的 ?name ?何時發(fā)生了變化,但卻非常昂貴。無論變化發(fā)生在何處,每個變化檢測周期都會以很大的頻率調(diào)用這個鉤子。在用戶可以執(zhí)行任何操作之前,本例中已經(jīng)調(diào)用了20多次。

這些初始化檢查大部分都是由 Angular 首次在頁面的其它地方渲染不相關(guān)的數(shù)據(jù)觸發(fā)的。只要把光標移動到另一個 ?<input>? 就會觸發(fā)一次調(diào)用。其中的少數(shù)調(diào)用揭示了相關(guān)數(shù)據(jù)的實際變化情況。如果使用這個鉤子,那么你的實現(xiàn)必須非常輕量級,否則會損害用戶體驗。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號