了解Angular的生命週期

元件(component)从建立到销毁的一整个生命週期当中,会经历数个阶段。

Angular提供了lifecycle hooks,让我们可以藉由对应每个生命周期阶段的方法执行程式码。

我们最常用的OnInit介面方法ngOnInit(),便是其中一个生命週期阶段所呼叫的方法。

生命週期执行顺序

http://img2.58codes.com/2024/201125731jPxblXbHG.png
图片来源

constructor

元件刚建立时呼叫。constructor通常是用来实作DI注入的,并不会在内部实作逻辑。constructor本质上不算lifecycle hooks。这边只是要说明它是元件建立之初最早被呼叫的方法。

ngOnChanges

元件中@Input/@Output所绑定的属性值改变时呼叫。只有使用@Input/@Output才会有ngOnChanges阶段。

ngOnInit

元件初始化时呼叫。在第一次ngOnChanges()完成之后呼叫,只调用一次。

AComponent

import { Component, OnInit } from '@angular/core';@Component({  selector: 'app-a',  templateUrl: './a.component.html',  styleUrls: ['./a.component.scss']})export class AComponent implements OnInit {  valueA = 0;  constructor() { }  ngOnInit(): void {  }  onAddValueA() {    this.valueA++;  }}

AComponent Template

<app-b [valueA]="valueA"></app-b><button (click)="onAddValueA()">AddValueA</button>

BComponent

import { Component, OnInit, Input, OnChanges } from '@angular/core';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnChanges {  @Input() valueA: number;  constructor() {    console.log('constructor called');  }  ngOnChanges() {    console.log('ngOnChanges called');  }  ngOnInit() {    console.log('ngOnInit called');  }}

BComponent Template

<p>Bcomponent valueA: {{ valueA }}</p>

AppComponent Template

<app-a></app-a>

AComponentBComponent的父元件,透过@Input()valueA值传给子元件。
BComponent,呼叫constructor()ngOnChanges()ngOnInit(),观察呼叫顺序:
http://img2.58codes.com/2024/20112573XYfRd5L8uR.png
可以得知呼叫顺序依序为:constructor() -> ngOnChanges() -> ngOnInit()

ngOnChanges()内,改为输出valueA的值:

ngOnChanges() {    console.log('ngOnChanges called valueA:', this.valueA);  }

http://img2.58codes.com/2024/20112573IGCRKDln7S.png

click Button:

可以发现,页面上的valueA有变化,但只有ngOnChanges()被呼叫,而其他方法未被呼叫。

这是因为ngOnInit()只会在元件建立后呼叫一次,而ngOnChanges()则是会根据@input()所绑定的属性值改变时呼叫。

纪录变化内容
呼叫ngOnChanges()时,可以藉由传入SimpleChange型别的参数,来取得@input()属性改变前后的值:

ngOnChanges(changes: SimpleChanges) {    console.log('ngOnChanges called ', this.valueA);    console.log(changes);  }

http://img2.58codes.com/2024/20112573EOuQGlmSx4.png
传入的物件内,属性为valueA,其型别为SimpleChange,有3个属性:

currentValue:当前的值firstChange: 只有第一次呼叫为true,之后都是falsepreviousValue: 上一次的值,第一次呼叫为undefined

ngDoCheck

发生变化检测(change detection)的情况时,呼叫ngDoCheckngDoCheck被呼叫的频率很高,成本高昂,这点要特别注意,以免影响使用者体验。

AComponent中的valueA,改成物件:

import { Component, OnInit } from '@angular/core';@Component({  selector: 'app-a',  templateUrl: './a.component.html',  styleUrls: ['./a.component.scss']})export class AComponent implements OnInit {  obj = { valueA: 0 };  constructor() { }  ngOnInit(): void {  }  onAddValueA() {    this.obj.valueA++;  }}

将物件传入BComponent

<app-b [obj]="obj"></app-b><button (click)="onAddValueA()">AddValueA</button>

BComponent中的@Input(),改为物件,实作ngDoCheck()

import { Component, OnInit, Input, OnChanges, SimpleChanges, DoCheck } from '@angular/core';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnChanges, DoCheck {  @Input() obj: { valueA: number };  constructor() {    console.log('constructor called');  }  ngOnChanges() {    console.log('ngOnChanges called obj.valueA:', this.obj.valueA);  }  ngOnInit() {    console.log('ngOnInit called');  }  ngDoCheck() {    console.log('ngDoCheck called obj.valueA:', this.obj.valueA);  }}

BComponent Template

<p>Bcomponent valueA: {{ obj.valueA }}</p>

click Button:

valueA确实如预期的增加3,但ngOnChanges()只呼叫一次,ngDoCheck()却每加一次就呼叫一次。

这是因为,所增加的只是@Input()物件里的属性值,并未改变obj物件的参考位址,所以Angular会判断@Input()物件并未变更,自然就不会呼叫ngOnChanges()

ngDoCheck()能做到像是这种Angular无法检测出的变化。

ngAfterContentInit

当 Angular 把外部内容投影(Content projection)至元件/指令的检视之后呼叫,第一次ngDoCheck()之后呼叫,只呼叫一次。内容投影(Content projection)是从父元件汇入HTML内容,并把它嵌入在子元件範本中指定位置的一种途径。实作上,我们可以在父元件输入想要的内容至子元件範本中的<ng-content>显示出来,增加子元件共用的弹性。

AComponent Template

<app-b>  <span>Bcomponent obj.valueA : {{ obj.valueA }}</span></app-b><button (click)="onAddValueA()">AddValueA</button>

将输入的内容(<span>Bcomponent obj.valueA : {{ obj.valueA }}</span>)放入<app-b>的Template。

BComponent Template

<p>  <ng-content></ng-content></p>

内容藉由<ng-content>显示于子元件。

BComponent

import { Component, OnInit, Input, OnChanges, SimpleChanges, DoCheck, AfterContentInit, AfterContentChecked } from '@angular/core';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnChanges, DoCheck, AfterContentInit {  //Input() obj: { valueA: number };  constructor() {    console.log('constructor called');  }  ngOnChanges(changes: SimpleChanges) {    console.log('ngOnChanges called');  }  ngOnInit() {    console.log('ngOnInit called');  }  ngDoCheck() {    console.log('ngDoCheck called');  }  ngAfterContentInit() {    console.log('ngAfterContentInit called');  }}

暂时不需要obj,先注解@Input()

依序显示:
http://img2.58codes.com/2024/20112573jvpUXUsTYB.png

可以发现,ngOnChanges()不见了,因为我们将@Input()拿掉,自然就不会有ngOnChanges阶段。

ngAfterContentChecked

每当被投影元件的内容变更后呼叫。ngAfterContentInit()和每次ngDoCheck()之后呼叫。

BComponent

import { Component, OnInit, Input, OnChanges, SimpleChanges, DoCheck, AfterContentInit, AfterContentChecked } from '@angular/core';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked {  //Input() obj: { valueA: number };  constructor() {    console.log('constructor called');  }  ngOnChanges(changes: SimpleChanges) {    console.log('ngOnChanges called');  }  ngOnInit() {    console.log('ngOnInit called');  }  ngDoCheck() {    console.log('ngDoCheck called');  }  ngAfterContentInit() {    console.log('ngAfterContentInit called');  }  ngAfterContentChecked() {    console.log('ngAfterContentChecked called');  }}

显示:
http://img2.58codes.com/2024/201125739So0WMEewT.png

click Button:

只有ngDoCheck()ngAfterContentChecked()被呼叫。
因为,父元件投射至子元件的内容改变,但并未销毁子元件,所以ngAfterContentChecked()被呼叫。
也因为子元件的内容改变了,自然会呼叫ngDoCheck()

ngAfterViewInit

元件检视及其子元件检视初始化完成之后呼叫。第一次ngAfterContentChecked()之后呼叫,只调用一次。

AComponent template

<app-b [obj]="obj"></app-b><button (click)="onAddValueA()">AddValueA</button>

新增CComponent

import { Component, OnInit, Input } from '@angular/core';@Component({  selector: 'app-c',  templateUrl: './c.component.html',  styleUrls: ['./c.component.scss']})export class CComponent implements OnInit {  @Input() valueA: number;  constructor() { }  ngOnInit(): void {  }}

CComponent template

<p>Ccomponent obj.valueA : {{ valueA }}</p>

BComponent Template

<app-c [valueA]="obj.valueA"></app-c>

修改BComponent,使用@ViewChild取得CComponent实体,我们尝试在不同的生命週期阶段取得子元件的实体:

import { Component, OnInit, Input, OnChanges, DoCheck, AfterContentInit,   AfterContentChecked, AfterViewInit, ViewChild } from '@angular/core';import { CComponent } from '../c/c.component';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked,  AfterViewInit {  @Input() obj: { valueA: number, valueB: number };  @ViewChild(CComponent) cComponent: CComponent;  constructor() {    console.log('constructor called');  }  ngOnChanges() {    console.log('ngOnChanges called');  }  ngOnInit() {    // 还未取得子元件实体    console.log('ngOnInit called : ', this.cComponent);  }  ngDoCheck() {    console.log('ngDoCheck called');    // 第一次呼叫时,还未取得子元件实体    console.log('ngDoCheck called', this.cComponent);  }  ngAfterContentInit() {    console.log('ngAfterContentInit called');  }  ngAfterContentChecked() {    console.log('ngAfterContentChecked called');  }  ngAfterViewInit() {    // 子元件的检视初始化完之后,取得子元件的实体    console.log('ngAfterViewInit called', this.cComponent);  }}

http://img2.58codes.com/2024/20112573phtGo7LzbE.png

ngOnInit阶段,子元件初始化还未完成,无法取得其实体。第一次ngDoCheck阶段,子元件初始化还未完成,无法取得其实体。ngAfterViewInit阶段,子元件初始化完成,取得其实体。BComponent@ViewChild绑定的cComponent改变,ngDoCheck再次被呼叫,此时可以取得子元件的实体,也会触发ngAfterContentChecked

从刚刚几个範例,可以看出ngDoCheck触发的频率很高,关于这点之后会另开篇幅说明。

ngAfterViewChecked

每当Angular做完元件检视和子检视的变更检测之后呼叫。ngAfterViewInit()和每次ngAfterContentChecked()之后呼叫。

BComponent新增ngAfterContentChecked()

ngAfterViewChecked() {    console.log('ngAfterViewChecked called', this.cComponent);  }

http://img2.58codes.com/2024/20112573zLwvqF3i07.png

click Button:

ngDoCheck阶段,还未侦测到子元件的变化。
直到ngAfterViewChecked阶段,才侦测到子元件的变化。

ngOnDestroy

Angular每次销毁指令/元件之前呼叫。在此阶段可取消订阅观察物件和分离事件处理器,以防记忆体洩漏。

当一个元件销毁时,内部的属性与方法也随之消失,但某些情况,正在执行的程式并不会停止,而是继续执行,这时我们就必须手动在元件销毁之前对其做处理,最常见的就是取消RxJS订阅。

AComponent template

<button (click)="display=!display">toggle Bcomponent</button><app-b *ngIf="display"></app-b>

利用button控制BComponent的建立/销毁。
AComponent

import { Component, OnInit } from '@angular/core';@Component({  selector: 'app-a',  templateUrl: './a.component.html',  styleUrls: ['./a.component.scss']})export class AComponent implements OnInit {  display = true;  constructor() { }  ngOnInit(): void {  }}

BComponent template

<p>counter : {{ counter }}</p>

BComponent

import { Component, OnInit } from '@angular/core';import { interval } from 'rxjs';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit {  counter = 0;  constructor() { }  ngOnInit() {    interval(1000).subscribe(val => {      this.counter++;      console.log(this.counter);    });  }}

使用RxJS的interval产生每秒送出一个递增1的数值的Observable,并且订阅它:

按下button将BComponent销毁后,可以看到interval依旧在执行,再次按下button,又产生新的订阅:

可以在ngOnDestroy()中,取消订阅:

import { Component, OnInit, OnDestroy } from '@angular/core';import { interval, Subscription } from 'rxjs';@Component({  selector: 'app-b',  templateUrl: './b.component.html',  styleUrls: ['./b.component.scss']})export class BComponent implements OnInit, OnDestroy {  counter = 0;  subscription: Subscription;  constructor() { }  ngOnInit() {    // 取得订阅    this.subscription = interval(1000).subscribe(val => {      this.counter = val;      console.log(this.counter);    });  }  ngOnDestroy() {    // 取消订阅    this.subscription.unsubscribe();  }}

随着BComponent的销毁,确实取消订阅,当BComponent再次建立时,新的订阅再次执行:

参考来源:
Angular-生命週期
[Angular 大师之路] Day 04 - 认识 Angular 的生命週期


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章