深入指南
元件

元件生命週期

TIP: 本指南假設你已讀過基礎指南。如果你是 Angular 新手,請先讀該指南。

元件的生命週期,是從元件建立到銷毀之間所經歷的一連串步驟。每個步驟代表 Angular 渲染元件、並隨時間檢查其更新的不同環節。

你可以在元件中實作生命週期鉤子,在這些步驟期間執行程式碼。與特定元件實例相關的生命週期鉤子,會以你元件類別上的方法來實作;與整個 Angular 應用程式相關的生命週期鉤子,則以接受回呼的函式形式實作。

元件的生命週期與 Angular 如何隨時間檢查元件的變更緊密相關。為了理解這個生命週期,你只需要知道:Angular 會由上而下走訪你的應用程式樹,檢查範本繫結是否有變更。以下說明的生命週期鉤子,會在 Angular 執行這趟走訪時執行。這趟走訪恰好造訪每個元件一次,因此你應避免在這個過程進行到一半時再更動狀態。

摘要

階段 方法 摘要
建立 constructor 標準 JavaScript 類別建構子 。在 Angular 具現化元件時執行。
變更
偵測
ngOnInit 在 Angular 初始化元件的所有 inputs 之後執行一次。
ngOnChanges 每當元件的 inputs 變更時執行。
ngDoCheck 每當檢查此元件是否有變更時執行。
ngAfterContentInit 在元件的 content 初始化之後執行一次。
ngAfterContentChecked 每當檢查此元件的內容是否有變更時執行。
ngAfterViewInit 在元件的 view 初始化之後執行一次。
ngAfterViewChecked 每當檢查元件的檢視是否有變更時執行。
渲染 afterNextRender 在下一次所有元件都渲染至 DOM 時執行一次。
afterEveryRender 每當所有元件都渲染至 DOM 時執行。
銷毀 ngOnDestroy 在元件銷毀前執行一次。

ngOnInit

ngOnInit 方法會在 Angular 以初始值初始化元件的所有 inputs 之後執行。元件的 ngOnInit 恰好執行一次。

這個步驟發生在元件自身的範本初始化_之前_。這表示你可以依據元件 inputs 的初始值來更新元件的狀態。

ngOnChanges

ngOnChanges 方法會在任何元件 inputs 變更之後執行。

這個步驟發生在檢查元件自身的範本_之前_。這表示你可以依據元件 inputs 的初始值來更新元件的狀態。

在初始化期間,第一個 ngOnChanges 會在 ngOnInit 之前執行。

檢視變更

ngOnChanges 方法接受一個 SimpleChanges 引數。這個物件是一個 Record,將每個元件 input 名稱對應到一個 SimpleChange 物件。每個 SimpleChange 都包含該 input 的前一個值、目前值,以及一個旗標,表示這是否為該 input 第一次變更。

你也可以選擇將目前的類別或 this 作為第一個泛型引數傳入,以獲得更嚴格的型別檢查。

@Component({/* ... */})
export class UserProfile {
  name = input('');

  ngOnChanges(changes: SimpleChanges<UserProfile>) {
    if (changes.name) {
      console.log(`Previous: ${changes.name.previousValue}`);
      console.log(`Current: ${changes.name.currentValue}`);
      console.log(`Is first ${changes.name.firstChange}`);
    }
  }
}

若你為任何 input 屬性提供了 alias,SimpleChanges 這個 Record 仍會以 TypeScript 屬性名稱作為鍵,而不是使用別名。

ngOnDestroy

ngOnDestroy 方法會在元件銷毀前執行一次。當元件不再顯示於頁面上時,Angular 會銷毀該元件,例如被 @if 隱藏,或導覽至其他頁面時。

DestroyRef

除了 ngOnDestroy 方法之外,你也可以注入 DestroyRef 的一個實例。呼叫 DestroyRef 的 onDestroy 方法,即可註冊一個在元件銷毀時被呼叫的回呼。

@Component({/* ... */})
export class UserProfile {
  constructor() {
    inject(DestroyRef).onDestroy(() => {
      console.log('UserProfile destruction');
    });
  }
}

你可以將 DestroyRef 實例傳給元件之外的函式或類別。當元件銷毀時,若有其他程式碼需要執行某些清理行為,請使用這個模式。

你也可以使用 DestroyRef,讓設定程式碼靠近清理程式碼,而不必將所有清理程式碼都放在 ngOnDestroy 方法中。

偵測實例銷毀

DestroyRef 提供了一個 destroyed 屬性,可用來檢查指定的實例是否已被銷毀。這在避免對已銷毀的元件進行操作時非常有用,特別是處理延遲或非同步邏輯時。

透過檢查 destroyRef.destroyed,你可以避免在實例清理完成後執行程式碼,從而避免如 NG0911: View has already been destroyed. 等潛在錯誤。

ngDoCheck

ngDoCheck 方法會在 Angular 每次檢查元件範本是否有變更之前執行。

你可以使用這個生命週期鉤子,在 Angular 一般的變更偵測之外手動檢查狀態變更,並手動更新元件的狀態。

這個方法執行得非常頻繁,可能大幅影響頁面的效能。請盡可能避免定義這個鉤子,只在別無選擇時才使用。

在初始化期間,第一個 ngDoCheck 會在 ngOnInit 之後執行。

ngAfterContentInit

ngAfterContentInit 方法會在巢狀置於元件內的所有子元素(其 content)初始化之後執行一次。

你可以使用這個生命週期鉤子來讀取內容查詢的結果。雖然你可以存取這些查詢已初始化的狀態,但請避免更動某個祖先元件的範本已經渲染過的非訊號狀態。這麼做可能導致 ExpressionChangedAfterItHasBeenCheckedError。

ngAfterContentChecked

ngAfterContentChecked 方法會在巢狀置於元件內的所有子元素(其 content)檢查完變更之後,每次都執行。

這個方法執行得非常頻繁,可能大幅影響頁面的效能。請盡可能避免定義這個鉤子,只在別無選擇時才使用。

雖然你可以在這裡存取內容查詢的更新後狀態,但請避免更動某個祖先元件的範本已經渲染過的非訊號狀態。這麼做可能導致 ExpressionChangedAfterItHasBeenCheckedError。

ngAfterViewInit

ngAfterViewInit 方法會在元件範本中的所有子元素(其 view)初始化之後執行一次。

你可以使用這個生命週期鉤子來讀取檢視查詢的結果。雖然你可以存取這些查詢已初始化的狀態,但請避免更動某個範本已經渲染過的非訊號狀態。這麼做可能導致 ExpressionChangedAfterItHasBeenCheckedError。

ngAfterViewChecked

ngAfterViewChecked 方法會在元件範本中的所有子元素(其 view)檢查完變更之後,每次都執行。

這個方法執行得非常頻繁,可能大幅影響頁面的效能。請盡可能避免定義這個鉤子,只在別無選擇時才使用。

雖然你可以在這裡存取檢視查詢的更新後狀態,但請避免更動某個範本已經渲染過的非訊號狀態。這麼做可能導致 ExpressionChangedAfterItHasBeenCheckedError。

afterEveryRender 與 afterNextRender

afterEveryRender 與 afterNextRender 函式可讓你註冊一個渲染回呼,在 Angular 完成將頁面上的_所有元件_渲染至 DOM 之後被呼叫。

這些函式與本指南中說明的其他生命週期鉤子不同。它們不是類別方法,而是接受回呼的獨立函式。渲染回呼的執行並不綁定任何特定的元件實例,而是應用程式層級的鉤子。

afterEveryRender 與 afterNextRender 必須在注入情境中呼叫,通常是在元件的建構子中。

你可以使用渲染回呼來執行手動的 DOM 操作。關於在 Angular 中處理 DOM 的指引,請參閱使用 DOM API。

渲染回呼不會在伺服器端渲染或建置時預渲染期間執行。

after*Render 的階段

使用 afterEveryRender 或 afterNextRender 時,你可以選擇將工作拆分為多個階段。階段讓你能控制 DOM 操作的先後順序,例如將_寫入_操作排在_讀取_操作之前,以盡量減少版面抖動。為了在階段之間溝通,階段函式可以傳回一個結果值,供下一個階段存取。

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

@Component(/* ... */)
export class UserProfile {
  private prevPadding = 0;
  private elementHeight = 0;

  constructor() {
    const elementRef = inject(ElementRef);
    const nativeElement = elementRef.nativeElement;

    afterNextRender({
      // Use the `Write` phase to write to a geometric property.
      write: () => {
        const padding = computePadding();
        const changed = padding !== this.prevPadding;
        if (changed) {
          nativeElement.style.padding = padding;
        }
        return changed; // Communicate whether anything changed to the read phase.
      },

      // Use the `Read` phase to read geometric properties after all writes have occurred.
      read: (didWrite) => {
        if (didWrite) {
          this.elementHeight = nativeElement.getBoundingClientRect().height;
        }
      },
    });
  }
}

共有四個階段,依以下順序執行:

階段 說明
earlyRead 使用此階段讀取任何會影響版面、且後續計算嚴格需要的 DOM 屬性與樣式。請盡可能避免使用此階段,優先使用 write 與 read 階段。
write 使用此階段寫入會影響版面的 DOM 屬性與樣式。
mixedReadWrite 預設階段。用於需要同時讀取與寫入會影響版面的屬性與樣式的操作。請盡可能避免使用此階段,優先使用明確的 write 與 read 階段。
read 使用此階段讀取任何會影響版面的 DOM 屬性。

生命週期介面

Angular 為每個生命週期方法提供了一個 TypeScript 介面。你可以選擇匯入並 implement 這些介面,確保你的實作沒有錯字或拼寫錯誤。

每個介面的名稱都與對應的方法相同,但不含 ng 前綴。例如,ngOnInit 的介面是 OnInit。

@Component({/* ... */})
export class UserProfile implements OnInit {
  ngOnInit() {
    /* ... */
  }
}

執行順序

以下圖表顯示 Angular 生命週期鉤子的執行順序。

初始化期間

Change detection

Rendering

constructor

ngOnChanges

ngOnInit

ngDoCheck

ngAfterContentInit

ngAfterViewInit

ngAfterContentChecked

ngAfterViewChecked

afterNextRender

afterEveryRender

後續更新

Change detection

Rendering

ngOnChanges

ngDoCheck

ngAfterContentChecked

ngAfterViewChecked

afterEveryRender

與指令的順序關係

當你在與元件相同的元素上放置一個或多個指令時,無論是在範本中還是透過 hostDirectives 屬性,框架都不保證在同一個元素上,某個生命週期鉤子在元件與指令之間的執行順序。切勿依賴觀察到的順序,因為這在 Angular 的後續版本中可能會改變。