元件(component)从建立到销毁的一整个生命週期当中,会经历数个阶段。
Angular提供了lifecycle hooks
,让我们可以藉由对应每个生命周期阶段的方法执行程式码。
我们最常用的OnInit
介面方法ngOnInit()
,便是其中一个生命週期阶段所呼叫的方法。
生命週期执行顺序
图片来源
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>
AComponent
为BComponent
的父元件,透过@Input()
将valueA
值传给子元件。
在BComponent
,呼叫constructor()
、ngOnChanges()
、ngOnInit()
,观察呼叫顺序:
可以得知呼叫顺序依序为:constructor()
-> ngOnChanges()
-> ngOnInit()
。
将ngOnChanges()
内,改为输出valueA
的值:
ngOnChanges() { console.log('ngOnChanges called valueA:', this.valueA); }
click Button:
可以发现,页面上的valueA
有变化,但只有ngOnChanges()
被呼叫,而其他方法未被呼叫。
这是因为ngOnInit()
只会在元件建立后呼叫一次,而ngOnChanges()
则是会根据@input()
所绑定的属性值改变时呼叫。
纪录变化内容
呼叫ngOnChanges()
时,可以藉由传入SimpleChange
型别的参数,来取得@input()
属性改变前后的值:
ngOnChanges(changes: SimpleChanges) { console.log('ngOnChanges called ', this.valueA); console.log(changes); }
传入的物件内,属性为valueA
,其型别为SimpleChange
,有3个属性:
currentValue
:当前的值firstChange
: 只有第一次呼叫为true
,之后都是false
previousValue
: 上一次的值,第一次呼叫为undefined
ngDoCheck
发生变化检测(change detection)的情况时,呼叫ngDoCheck
。ngDoCheck
被呼叫的频率很高,成本高昂,这点要特别注意,以免影响使用者体验。将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()
依序显示:
可以发现,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'); }}
显示:
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); }}
ngOnInit
阶段,子元件初始化还未完成,无法取得其实体。第一次ngDoCheck
阶段,子元件初始化还未完成,无法取得其实体。ngAfterViewInit
阶段,子元件初始化完成,取得其实体。BComponent@ViewChild
绑定的cComponent
改变,ngDoCheck
再次被呼叫,此时可以取得子元件的实体,也会触发ngAfterContentChecked
。从刚刚几个範例,可以看出ngDoCheck
触发的频率很高,关于这点之后会另开篇幅说明。
ngAfterViewChecked
每当Angular做完元件检视和子检视的变更检测之后呼叫。ngAfterViewInit()
和每次ngAfterContentChecked()
之后呼叫。在BComponent
新增ngAfterContentChecked()
ngAfterViewChecked() { console.log('ngAfterViewChecked called', this.cComponent); }
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 的生命週期