Angular生命週期實踐
生命週期hooks的意義
在Angular中,實現元件的生命週期hook介面方法是使用Angular框架實現插入業務的時機點。
官方文件中生命週期都有哪些內容
在Angular的官方文件中有一個章節Lifecycle hooks來專門講解生命週期的機制與如何來觀察元件生命週期。
從官方章節中,我們能瞭解到生命週期hook方法 的方方面面並且基本不會有出入。
我列舉幾點從官方收穫到的:
- 在元件中的寫法;要實現
OnInit
方法,在元件中新增ngOnInit
方法, 注意多了倆個字母, 每個hook方法均是如此。 - 生命週期事件順序;
- 如何去觀察生命週期事件;
- 使用ngOnInit作為業務插入點,而不應該在constructor中放入業務程式碼;
- ngOnDestroy的執行時機
- 全部生命週期事件觸發順序
- ngOnChanges的執行機制
- ...等等
這基本上覆蓋到了有元件生命週期的方方面面;
本文重點內容
官方章節內容是基於生命週期本身去講的,而實際使用過程生命週期的執行過程會與依賴注入時機, 資料流傳遞,變化檢測,父子元件資料變更,元件繼承等等諸多特性結合使用,在結合後,執行順序是怎樣的對我們來說有點不那麼容易弄明白,只有通過大量思考以及實踐後才能得知並總結出其規律。
與官方的切入點不同,本文希望從實踐中來總結規律。
而本文選取了如下幾種場景,進行實踐演示,並嘗試得出規律:
- 基本的生命週期介紹
- 資料流傳遞的時機是在哪個具體的事件執行。
- 父子元件中父子元件的生命週期是如何執行的。
- 繼承元件中生命週期又是如何執行的。
- 模板中繫結的變數在獲取值時與這些生命週期有沒有關係?
希望通過本文的閱讀,能幫你撥開一些面紗,讓你能夠隨我一起進一步掌握Angular的特性,去思考Angular中生命週期設定去窺探Angular的執行機制;理解Angular的思路、思想以及其為開發者塑造的思考模式。
只有我們能夠按照Angular的方式思考,當遇到複雜的業務參與了複雜的資料流,複雜的元件關係,複雜的類關係後,能通過思考快速瞭解知識盲區,快速組織獲取相關知識的關鍵詞,這樣茫茫Angular的概念以及新知識的海洋裡你就變得如魚得水,也會成為我們開發業務程式碼時,優化重構,解決問題時最鋒利的矛。
lifecycle-hook-basic
由於官方文件中已經把生命週期的順序介紹並演示了,我們這裡就做個簡單的驗證和總結,當做一次複習。
首先來看一個實際執行效果圖
注意:有仔細的小夥伴可能會注意到這個圖中,沒有看到ngOnChanges事件。不是忘了 根元件不會觸發ngOnChanges事件,因為跟元件沒有@Input變數;
原始碼如下
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular ' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {
this.subject.subscribe((item) => {
this.messages.push(item);
});
this.subject.next({ type: 'constructor exec', content: 'AppComponent class instance' });
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnChanges' });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnInit' });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngDoCheck' });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentInit' });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentChecked' });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewInit' });
}
ngAfterViewChecked() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewChecked' });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnDestroy' });
}
}
下圖是正常元件,並帶有@Input屬性
原始碼如下
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular ' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {
this.subject.subscribe((item) => {
this.messages.push(item);
});
this.subject.next({ type: 'constructor exec', content: 'AppComponent class instance' });
}
}
// HelloComponent 是AppComponent的子檢視元件
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {
this.subject.next({ type: '@input', content: 'set name update' });
this._name = n;
}
get name() {
// this.subject.next({ type: 'template binding variable get', content: 'get name update' }); 僅演示呼叫
return this._name;
}
messages = [];
constructor() {
this.subject.next({ type: 'constructor exec', content: 'class instance, 訪問@input屬性name=' + this.name });
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 訪問@input屬性name=' + this.name });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 訪問@input屬性name=' + this.name });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 訪問@input屬性name=' + this.name });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 訪問@input屬性name=' + this.name });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 訪問@input屬性name=' + this.name });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 訪問@input屬性name=' + this.name });
}
ngAfterViewChecked() {
this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 訪問@input屬性name=' + this.name });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 訪問@input屬性name=' + this.name });
}
}
解釋並歸納
我們結合上面順序可以先粗略畫一個圖如下:
解釋下這個圖中的每個鉤子的含義:
注意:
- 淺灰色名字的事件,在元件的生命週期中只會觸發一次,而綠色的隨著相應的邏輯變化會多次觸發。
- 這裡我將元件建構函式的執行也加入到了觀察序列中,因為在業務中,經常會有小夥伴會在constructor中插入業務程式碼。
所有的方法執行順序如下:
Construction, OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy
Construction
建構函式執行時, 執行一次。
OnChanges
當指令(元件的實現繼承了指令)的任何一個可繫結屬性發生變化時呼叫。
要點:
- 從上面根元件的執行效果來看,這個hook不一定會被呼叫,只有@Input屬性變化才會觸發;
- @Input屬性的變更次數是沒有要求的,所以它的回撥次數也是沒有限制的。
- 這塊要特別注意父級改變傳遞值的情況,會導致ngOnChange在生命週期的任何時刻都可能被再次呼叫。
- 引數是一個輸入屬性的變更處理器,會包含所有變更的輸入屬性的舊值和新值;
- 如果至少發生了一次變更,則該回撥方法會在預設的變更檢測器檢查完可繫結屬性之後、檢視子節點和內容子節點檢查完之前呼叫。
- 屬性的setter能夠替代在這個鉤子中執行邏輯。
OnInit
在 Angular 初始化完了該指令的所有資料繫結屬性之後呼叫;
要點:
- 在預設的變更檢測器首次檢查完該指令的所有資料繫結屬性之後,任何子檢視或投影內容檢查完之前。
- 它會且只會在指令初始化時呼叫一次
- 定義
ngOnInit()
方法可以處理所有附加的初始化任務。
DoCheck
在變更檢測期間,預設的變更檢測演算法會根據引用來比較可繫結屬性,以查詢差異。 你可以使用此鉤子來用其他方式檢查和響應變更。
要點:
- 除了使用預設的變更檢查器執行檢查之外,還會為指令執行自定義的變更檢測函式
- 預設變更檢測器檢查更改時,他會觸發OnChanges()的執行(如果有),而不在乎你是否進行了額外的變更檢測。
- 不應該同時使用DoCheck和OnChanges來響應在同一個輸入上發生的更改。
- 預設的變更檢測器執行之後呼叫,並進行變更檢測。
- 參見
KeyValueDiffers
和IterableDiffers
,以實現針對集合物件的自定義變更檢測邏輯。 - 在DoCheck中你可以實現監控那些OnChanges無法捕獲的變更,檢測的邏輯需要自行實現。
- 由於DoCheck可以監控出特定變數的何時發生了變化,但這卻非常昂貴。Angular 在頁面的其它地方渲染不相關的資料也會觸發這個鉤子,所以你的實現必須自行保證使用者體驗。
AfterContentInit
它會在 Angular 初始化完該指令的所有內容之後立即呼叫。
要點:
- 在指令初始化完成之後,它只會呼叫一次。
- 可以用來處理一些初始化任務
AfterContentChecked
在預設的變更檢測器對該指令下的所有內容完成了變更檢測之後立即呼叫。
AfterViewInit
- 在 Angular 完全初始化了元件的檢視後呼叫。 定義一個
ngAfterViewInit()
方法來處理一些額外的初始化任務。
AfterViewChecked
在預設的變更檢測器對元件檢視完成了一輪變更檢測週期之後立即呼叫。
OnDestroy
在指令、管道或服務被銷燬時呼叫。 用於在例項被銷燬時,執行一些自定義清理程式碼。
進一步說明:
因為我們關心的是什麼時候使用這些鉤子,大家回到上面這些回撥鉤子定義處,仔細觀察帶有Init的鉤子內容,可以看到OnInit , AfterContentInit, AfterViewInit 這三個,他們都只執行一次,都可以做一些初始化任務,這三個鉤子的區別,就像定義中的描述都是有差別的。
大家有需要可以在文章下面留言,看實際情況是否需要仔細解釋下,這三個鉤子所適用的差異化場景。
沒有仔細研究過這三者不同的小夥伴,常常對於應該把自己要實現的非同步初始化業務放到哪個鉤子中暈頭轉向,所以隨便選一個,要麼放在OnInit, 要麼AfterViewInit,如果發現放到一個裡不行,就換另一個,直到問題解決或耗費很長時間問題解決不了或者留下偶現的bug(之所以偶現是因為沒有從技術上保證非同步的順序性執行),再次排查也相當費勁。
所以這三個Init非常值得寫非同步業務比較多的小夥伴關注。
從第一個場景下,我們回顧了每一個生命週期鉤子都有哪些內容。
接下來我們看一下帶有@Input的場景:
lifecycle-hook&Input
原始碼如下
//AppComponent html
<h1>Hi, Angular 13!</h1>
<h3>- 演示生命週期鉤子函式呼叫順序<br /></h3>
<p>Start editing to see some magic happen :)</p>
<ul>
<li *ngFor="let message of messages">
<span class="message-type">{{ message.type }}</span>
=>
<span class="message-content">{{ message.content }}</span>
</li>
</ul>
<hello [name]="name"></hello>
// AppComponent
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent{
name = 'Angular ' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {
this.subject.subscribe((item) => { this.messages.push(item); });
this.subject.next({ type: 'constructor exec', content: 'AppComponent class instance, 訪問@input屬性name=' + this.name });
}
}
// HelloComponent 是AppComponent的子檢視元件
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {
this.subject.next({ type: '@input', content: 'set name update' });
this._name = n;
}
get name() {
// this.subject.next({ type: 'template binding variable get', content: 'get name update' }); 僅演示呼叫
return this._name;
}
messages = [];
constructor() {
this.subject.next({ type: 'constructor exec', content: 'class instance, 訪問@input屬性name=' + this.name });
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 訪問@input屬性name=' + this.name });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 訪問@input屬性name=' + this.name });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 訪問@input屬性name=' + this.name });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 訪問@input屬性name=' + this.name });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 訪問@input屬性name=' + this.name });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 訪問@input屬性name=' + this.name });
}
ngAfterViewChecked() {
this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 訪問@input屬性name=' + this.name });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 訪問@input屬性name=' + this.name });
}
}
執行結果
解釋並歸納
我們給Hello元件新增了輸入屬性,父元件AppComponent初始化時給name賦值了初始值,僅在angular的處理下輸入屬性的繫結是發生在Hello元件初始化之前(當然在Hello元件生命週期呼叫的過程中,父元件隨時可能改變name)。
- 注意在第一次OnChanges觸發之後,也就是傳遞變數的初始值給完後,有些情況我們會通過邏輯在父元件中調整傳遞變數的值,這時就會立即再次觸發OnChanges的回撥,並且這個回撥與HelloComponent元件的OnInit,AfterContentInit等的回撥是按時間順序依次呼叫的。也就是OnChanges的觸發與AfterContentInit, AfterViewInit是否已經完成一次執行無關。
我們在剛剛的AppComponent元件中也加入生命週期的執行,結果會怎麼樣呢?
lifecycle-hook&child&parent&Input
改動原始碼如下
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular ' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {
this.subject.subscribe((item) => { this.messages.push(item); });
this.subject.next({ type: 'constructor exec', content: 'AppComponent class instance, 訪問@input屬性name=' + this.name });
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnChanges' });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnInit' });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngDoCheck' });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentInit' });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentChecked' });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewInit' });
}
ngAfterViewChecked() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewChecked' });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnDestroy' });
}
}
執行結果
解釋並歸納
現象:
- 父元件AppComponent的生命週期回撥被子元件分為了倆部分。
- constuctor的執行順序是先父元件,接下來是子元件,接下來才是生命週期的其他函式回撥。
- 父元件的ngOnChanges(如果有,第一次), ngOnInit,ngDoCheck,ngAfterContentInit,ngAfterContentChecked會執行較早。
- 父元件name值傳遞到子元件,觸發子元件的OnChanges。
- 子元件的生命週期執行,接下來父元件的ngAfterViewInit, ngAfterViewChecked執行。
要點:
父元件將繫結值傳入到子元件是在父元件的生命週期執行到ngAfterContentChecked時觸發,這一點很重要
- 意味著,如果在子元件的生命週期(比如:OnInit)中有處理依賴傳遞變數的邏輯,那麼可能得不到最新的傳遞值。(由於這一點,小夥伴經常陷入困惑中,這也與不瞭解Init鉤子的適用場景有關)
- 父子元件中,AfterViewInit會等到所有的子元件的生命週期執行完成才執行,(這一點特性應該被充分發揮並利用)。
接下來我們看看存在繼承元件的場景下,Angular會怎麼處理生命週期回撥。
life-hook&child&parent&inheritComponent&input
改動原始碼如下:
...
// 這一次我們新增了一個BaseComponent作為Hello元件的基類,在Angular中是以Directive來裝飾的
// 使用Directive的好處
// Angular元件繼承不會繼承後設資料,可以使用directive裝飾器後設資料可配置空來避免配置多餘的後設資料
// Directive是Component裝飾器的基類,基本無縫替換
@Directive()
export class BaseComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
subject = window['subject'];
constructor() {
this.subject.next({ type: 'constructor exec', content: 'BaseComponent class instance' });
}
_name: string = '';
@Input() set name(n: string) {
this.subject.next({ type: '@input', content: 'set base name update' });
this._name = n;
}
get name() {
// 非必要不定義getter或者不放邏輯,訪問次數非常多
// this.subject.next({ type: 'tpl binding variable get', content: 'get name update' });
return this._name;
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnChanges' });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnInit' });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngDoCheck' });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterContentInit' });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterContentChecked' });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterViewInit' });
}
ngAfterViewChecked() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterViewChecked' });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnDestroy' });
}
}
// HelloComponent 是AppComponent的子檢視元件, 同時也是BaseComponent的子類
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent extends BaseComponent {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {
this.subject.next({ type: '@input', content: 'set name update' });
this._name = n;
}
get name() {
// this.subject.next({ type: 'template binding variable get', content: 'get name update' }); 僅演示呼叫
return this._name;
}
messages = [];
constructor() {
super();
this.subject.next({ type: 'constructor exec', content: 'class instance, 訪問@input屬性name=' + this.name });
}
}
執行結果
解釋歸納
現象及實現情況
- 生命週期去掉了繼承體系的HelloComponent中的實現,在基類BaseComponent中實現
- 無論如何構造器都是在最早執行併案依賴順序執行
- BaseComponent中的生命週期都執行了(我們知道繼承後,這些生命週期方法在Hello元件中也是可以呼叫的,那Angular到底呼叫的是子類的還是基類的呢?請繼續往下看)
繼續修改原始碼:
// HelloComponent 是AppComponent的子檢視元件, 同時也是BaseComponent的子類
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato; }`],
})
export class HelloComponent extends BaseComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy{
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {
this.subject.next({ type: '@input', content: 'set name update' });
this._name = n;
}
get name() {
// this.subject.next({ type: 'template binding variable get', content: 'get name update' }); 僅演示呼叫
return this._name;
}
messages = [];
constructor() {
super();
this.subject.next({ type: 'constructor exec', content: 'class instance, 訪問@input屬性name=' + this.name });
}
ngOnChanges() {
this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 訪問@input屬性name=' + this.name });
}
ngOnInit() {
this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 訪問@input屬性name=' + this.name });
}
ngDoCheck() {
this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 訪問@input屬性name=' + this.name });
}
ngAfterContentInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 訪問@input屬性name=' + this.name });
}
ngAfterContentChecked() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 訪問@input屬性name=' + this.name });
}
ngAfterViewInit() {
this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 訪問@input屬性name=' + this.name });
}
ngAfterViewChecked() {
this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 訪問@input屬性name=' + this.name });
}
ngOnDestroy() {
this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 訪問@input屬性name=' + this.name });
}
}
我們為子類也實現這些生命週期,看看Angualr執行的是子類的還是父類的還是倆個都會執行,執行的順序如何?
執行結果
歸納總結:
現象是執行了子類的,父類的沒有執行
- 說明生命週期方法,子類和父類之間生命週期方法也符合繼承原理存在重寫的情況,子類中的生命週期方法
模板繫結變數獲取的情況
最後順道再看一下模板繫結的變數Angular獲取的情況,如下圖
get name() { // 只需要把name的getter中的註釋去掉即可
this.subject.next({ type: 'template binding variable get', content: 'get name update' }); 僅演示呼叫
return this._name;
}
// 另一個,需要把生命週期鉤子中列印name欄位讀取去掉,這樣我們就知道name被Angular讀取了幾次,並在什麼時候讀取。(有時候,我們會在getter中寫一些簡單的邏輯,把變數作為計算屬性,瞭解這個對我們知曉name被讀取的數量,有很大用處)
解釋並歸納
- 父元件將name屬性傳遞給Hello元件(@input執行)後,Hello元件將自己的內容ngAfterContentChecked後獲取name, 並在App元件ngAfterViewChecked後讀取了name的值,整個過程讀取了倆次。
最後再附上一張關於結合上述三個元件的Angular專案的執行以及生命週期執行運轉圖
最後,如果大家對上述表述或者結論存在質疑或者不解,可以在下面留言。