一苏遥、生命周期鉤子
每個(gè)組件都有一個(gè)被 Angular 管理的生命周期供填。
Angular 創(chuàng)建它,渲染它婿着,創(chuàng)建并渲染它的子組件颤绕,在它被綁定的屬性發(fā)生變化>時(shí)檢查它幸海,并在它從 DOM 中被移除前銷毀它。
Angular 提供了生命周期鉤子奥务,把這些關(guān)鍵生命時(shí)刻暴露出來(lái),賦予你在它們發(fā)生時(shí)采取行動(dòng)的能力袜硫。
除了那些組件內(nèi)容和視圖相關(guān)的鉤子外,指令有相同生命周期鉤子氯葬。
二、組件生命周期鉤子概覽
指令和組件的實(shí)例有一個(gè)生命周期:新建婉陷、更新和銷毀帚称。 通過(guò)實(shí)現(xiàn)一個(gè)或多個(gè) Angular core
庫(kù)里定義的生命周期鉤子接口,開(kāi)發(fā)者可以介入該生命周期中的這些關(guān)鍵時(shí)刻秽澳。
每個(gè)接口都有唯一的一個(gè)鉤子方法闯睹,它們的名字是由接口名再加上 ng
前綴構(gòu)成的。比如担神,OnInit
接口的鉤子方法叫做 ngOnInit
楼吃, Angular 在創(chuàng)建組件后立刻調(diào)用它,
沒(méi)有指令或者組件會(huì)實(shí)現(xiàn)所有這些接口妄讯,并且有些鉤子只對(duì)組件有意義孩锡。只有在指令/組件中定義過(guò)的那些鉤子方法才會(huì)被 Angular 調(diào)用。
三亥贸、生命周期的順序
1.組件生命周期概覽
紅色的被調(diào)用一次躬窜,綠色的會(huì)被調(diào)用多次。
這里分為了三個(gè)階段炕置,
組件初始化階段
荣挨,變化檢測(cè)
,組件銷毀
朴摊。會(huì)在組件初始化后看到組件默垄,在變化檢測(cè)階段讓屬性值和頁(yè)面展示保持一致。
變化檢測(cè)中的四個(gè)方法和組件初始化中的四個(gè)方法是一樣的仍劈。
一共只有9個(gè)方法厕倍。
當(dāng) Angular 使用構(gòu)造函數(shù)新建一個(gè)組件或指令后,就會(huì)按下面的順序在特定時(shí)刻調(diào)用這些生命周期鉤子方法:
鉤子 | 用途及時(shí)機(jī) |
---|---|
ngOnChanges() |
當(dāng) Angular(重新)設(shè)置數(shù)據(jù)綁定輸入屬性時(shí)響應(yīng)贩疙。 該方法接受當(dāng)前和上一屬性值的 SimpleChanges 對(duì)象當(dāng)被綁定的輸入屬性的值發(fā)生變化時(shí)調(diào)用讹弯,首次調(diào)用一定會(huì)發(fā)生在 ngOnInit() 之前。 |
ngOnInit() |
在 Angular 第一次顯示數(shù)據(jù)綁定和設(shè)置指令/組件的輸入屬性之后这溅,初始化指令/組件组民。在第一輪 ngOnChanges() 完成之后調(diào)用,只調(diào)用一次悲靴。 |
ngDoCheck() |
檢測(cè)臭胜,并在發(fā)生 Angular 無(wú)法或不愿意自己檢測(cè)的變化時(shí)作出反應(yīng)。在每個(gè) Angular 變更檢測(cè)周期中調(diào)用,ngOnChanges() 和 ngOnInit() 之后耸三。 |
ngAfterContentInit() |
當(dāng)把內(nèi)容投影進(jìn)組件之后調(diào)用乱陡。第一次 ngDoCheck() 之后調(diào)用,只調(diào)用一次仪壮。 |
ngAfterContentChecked() |
每次完成被投影組件內(nèi)容的變更檢測(cè)之后調(diào)用憨颠。ngAfterContentInit() 和每次 ngDoCheck() 之后調(diào)用 |
ngAfterViewInit() |
初始化完組件視圖及其子視圖之后調(diào)用。第一次 ngAfterContentChecked() 之后調(diào)用积锅,只調(diào)用一次爽彤。 |
ngAfterViewChecked() |
每次做完組件視圖和子視圖的變更檢測(cè)之后調(diào)用。ngAfterViewInit() 和每次 ngAfterContentChecked() 之后調(diào)用缚陷。 |
ngOnDestroy() |
當(dāng) Angular 每次銷毀指令/組件之前調(diào)用并清掃适篙。 在這兒反訂閱可觀察對(duì)象和分離事件處理器,以防內(nèi)存泄漏箫爷。在 Angular 銷毀指令/組件之前調(diào)用嚷节。 |
2.代碼示例
新建hooks組件
ng g hooks
//app.component.html
<app-hooks [name]="title"></app-hooks>
//app.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title: string = 'XiaoMing';
}
//hooks.component.html
<p>
{{name}}
</p>
//hooks.component.ts
///<reference path="../../../node_modules/@angular/core/src/metadata/lifecycle_hooks.d.ts"/>
import {
AfterContentChecked, AfterContentInit, AfterViewChecked, AfterViewInit, Component, DoCheck, Input, OnChanges, OnDestroy,
OnInit, SimpleChanges
} from '@angular/core';
let logIndex: number = 1;
@Component({
selector: 'app-hooks',
templateUrl: './hooks.component.html',
styleUrls: ['./hooks.component.css']
})
export class HooksComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy {
@Input()
name: string;
logMsg(msg: string) {
console.log(`#${logIndex++} ${msg}`);
}
constructor() {
this.logMsg('name屬性在construstor里的值是' + name);
}
ngOnChanges(changes: SimpleChanges): void {
const newName = changes['name'].currentValue;
this.logMsg('name屬性在ngOnChanges里的值是' + newName);
}
ngOnInit(): void {
this.logMsg('ngOnInit');
}
ngDoCheck(): void {
this.logMsg('ngDoCheck');
}
ngAfterContentInit(): void {
this.logMsg('ngAfterContentInit');
}
ngAfterContentChecked(): void {
this.logMsg('ngAfterContentChecked');
}
ngAfterViewInit(): void {
this.logMsg('ngAfterViewInit');
}
ngAfterViewChecked(): void {
this.logMsg('ngAfterViewChecked');
}
ngOnDestroy(): void {
this.logMsg('ngOnDestroy');
}
}
調(diào)用順序如圖所示
首先會(huì)調(diào)用構(gòu)造函數(shù)
ngOnChanges:當(dāng)一個(gè)父組件修改或初始化一個(gè)子組件的`輸入屬性`的時(shí)候被調(diào)用
ngOnInit:初始化(如果初始化的邏輯需要依賴輸入屬性,那就一定要寫(xiě)在ngOnInit中蝶缀,而不要寫(xiě)在構(gòu)造函數(shù)中)
ngDoCheck:用來(lái)檢測(cè)
ngAfterContentInit:
ngAfterContentChecked:
ngAfterViewInit
ngAfterViewChecked
ngDoCheck
ngAfterContentChecked
ngAfterViewChecked
四丹喻、具體鉤子
4.1 ngOnchanges
當(dāng)
父組件初始化
或修改子組件的輸入?yún)?shù)
時(shí)會(huì)被調(diào)用。
可變對(duì)象翁都,不可變對(duì)象
字符串是不可變的(改變值只是修改了內(nèi)存中的指向)
對(duì)象的值是可變的
代碼示例
新建child組件
ng g component child
//child.component.html
<div style="background: deepskyblue;">
<h2>我是子組件</h2>
<div>問(wèn)候語(yǔ):{{greeting}}</div>
<div>姓名:{{user.name}}</div>
<div>消息:<input [(ngModel)]="message"></div>
</div>
//child.component.ts
import {Component, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges {
@Input()
greeting: string;
@Input()
user: { name: string };
message: string = '初始化消息';
constructor() {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
console.log(JSON.stringify(changes, null, 4));
}
}
//app.component.html
<div style="background: deeppink">
<h1>
我是主組件
</h1>
問(wèn)候語(yǔ):<input type="text" [(ngModel)]="greeting">
姓名:<input type="text" [(ngModel)]="user.name">
<app-child [greeting]="greeting" [user]="user"></app-child>
</div>
//app.component.ts
import {Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
greeting: string = 'Hi';
user: { name: string } = {name: 'XiaoMing'};
}
運(yùn)行結(jié)果:
改變父組件‘問(wèn)候語(yǔ)’的值為'Hi!'
但當(dāng)我們改變父組件中'姓名'的值為'XiaoMing11111'時(shí)碍论,控制臺(tái)不會(huì)打印出新東西。
因?yàn)間reeting是字符串是不可變對(duì)象
(每次值改變的時(shí)候都會(huì)創(chuàng)建一個(gè)新的字符串柄慰,然后把引用指向新的字符串)鳍悠,而user是可變對(duì)象
,修改姓名的值的時(shí)候并沒(méi)有改變user對(duì)象的引用坐搔。那么怎么監(jiān)控可變對(duì)象呢藏研,用doCheck
當(dāng)我改變頁(yè)面中子組件的消息時(shí),也不會(huì)打印出新東西概行,因?yàn)樽咏M件中的message屬性沒(méi)有被@Input()標(biāo)記不是輸入屬性蠢挡。
4.2 變更檢測(cè)機(jī)制和DoCheck鉤子
查看package.json文件中的dependencies的zone.js
就是zone.js
來(lái)實(shí)現(xiàn)變更檢測(cè)機(jī)制
的,主要作用是保證屬性的變化和頁(yè)面的變化是一致的
凳忙,瀏覽器中發(fā)生的任何異步事件都會(huì)觸發(fā)變更檢測(cè)业踏,比如點(diǎn)擊按鈕,輸入數(shù)據(jù)...
默認(rèn)的是
Default
策略涧卵,當(dāng)父組件變化時(shí)會(huì)檢測(cè)整個(gè)組件樹(shù)
如果在子組件中設(shè)置了
OnPush
勤家,當(dāng)父組件變化時(shí)就只會(huì)檢測(cè)父組件
。
當(dāng)孫子組件1發(fā)生變化后柳恐,紅色的部分都會(huì)被檢測(cè)一遍伐脖,也就是調(diào)DoCheck方法热幔。并且是從父組件開(kāi)始檢查。
書(shū)接上文讼庇,我們?cè)赾hild組件中添加Docheck鉤子
//child.component.ts
import {Component, DoCheck, Input, OnChanges, OnInit, SimpleChanges} from '@angular/core';
@Component({
selector: 'app-child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.css']
})
export class ChildComponent implements OnInit, OnChanges, DoCheck {
@Input()
greeting: string;
@Input()
user: { name: string };
message: string = '初始化消息';
OldUserName: string;
changeDetected: boolean = false;
noChangeCount: number = 0;
constructor() {
}
ngOnInit() {
}
ngOnChanges(changes: SimpleChanges): void {
console.log(JSON.stringify(changes, null, 4));
}
ngDoCheck(): void {
if (this.OldUserName !== this.user.name) {
this.changeDetected = true;
console.log('DoCheck:user.name從' + this.OldUserName + '變?yōu)? + this.user.name);
this.OldUserName = this.user.name;
}
if (this.changeDetected) {
this.noChangeCount = 0;
} else {
this.noChangeCount += 1;
console.log('DoCheck:user.name沒(méi)變化時(shí)ngDoCheck方法已經(jīng)被調(diào)用' + this.noChangeCount + '次');
}
this.changeDetected = false;
}
}
DoCheck運(yùn)行效果:
- 頁(yè)面初始化時(shí)
docheck
調(diào)用一次
- 當(dāng)我在問(wèn)候語(yǔ)的輸入框和姓名的輸入框中來(lái)回切換點(diǎn)擊的時(shí)候绎巨,就會(huì)觸發(fā)
docheck
方法。當(dāng)我改變姓名的值的時(shí)候也會(huì)觸發(fā)巫俺。之后我再點(diǎn)擊輸入框的時(shí)候认烁,調(diào)用次數(shù)重置為1。這就是上一段代碼要實(shí)現(xiàn)的效果介汹。
雖然當(dāng)我修改姓名的時(shí)候這個(gè)鉤子會(huì)被調(diào)用,但是我們必須要小心ngdocheck這個(gè)鉤子會(huì)
非常頻繁
的被調(diào)用舶沛,每一次變化都會(huì)被調(diào)用嘹承,在這個(gè)例子中,我還沒(méi)做任何操作呢如庭,只是在頁(yè)面隨便點(diǎn)點(diǎn)就會(huì)被調(diào)用好幾次叹卷,只有很少的調(diào)用次數(shù)是修改數(shù)據(jù)的時(shí)候觸發(fā)的。所以對(duì)ngDoCheck這個(gè)方法的實(shí)現(xiàn)一定要
非常高效
坪它,非常輕量級(jí)骤竹,不然會(huì)引起性能問(wèn)題。不光是這個(gè)方法往毡,變更檢測(cè)中的那些帶Check
的方法都應(yīng)該這樣
4.3 view鉤子
如何在父組件
中調(diào)用子組件
中的方法
新建一個(gè)子組件
ng g component child2
//child2.component.ts
import {Component, OnInit} from '@angular/core';
@Component({
selector: 'app-child2',
templateUrl: './child2.component.html',
styleUrls: ['./child2.component.css']
})
export class Child2Component implements OnInit {
constructor() {
}
ngOnInit() {
}
greeting(name: string) {
console.log('Hello' + name);
}
}
//app.component.html
<h1>主組件</h1>
<div>
<app-child2 #view1></app-child2>
<app-child2 #view1></app-child2>
<button (click)="view1.greeting('William')">點(diǎn)擊按鈕</button>
</div>
//app.component.ts
import {Component, ViewChild} from '@angular/core';
import {Child2Component} from './child2/child2.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
@ViewChild('view1')
view1: Child2Component;//獲得子組件之后就可以調(diào)用子組件中的方法了
constructor() {}
ngOnInit(): void {
this.view1.greeting('XiaoMing');//調(diào)用方法
}
}
運(yùn)行效果:
會(huì)在控制臺(tái)打印出HelloXiaoMing
然后我點(diǎn)擊按鈕蒙揣,會(huì)打印出HelloWilliam
這樣就實(shí)現(xiàn)了在父組件中調(diào)用子組件方法。
現(xiàn)在學(xué)習(xí)那兩個(gè)鉤子AfterViewInit
开瞭,AfterViewChecked
//修改child2.component.ts
import {AfterViewChecked, AfterViewInit, Component, OnInit} from '@angular/core';
@Component({
selector: 'app-child2',
templateUrl: './child2.component.html',
styleUrls: ['./child2.component.css']
})
export class Child2Component implements OnInit, AfterViewInit, AfterViewChecked {
constructor() {
}
ngOnInit() {
}
ngAfterViewInit(): void {
console.log('子組件的視圖初始化完畢');
}
ngAfterViewChecked(): void {
console.log('子組件的視圖變更檢測(cè)完畢');
}
greeting(name: string) {
console.log('Hello' + name);
}
}
//修改app.component.ts
import {AfterViewChecked, AfterViewInit, Component, OnInit, ViewChild} from '@angular/core';
import {Child2Component} from './child2/child2.component';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, AfterViewInit, AfterViewChecked {
@ViewChild('view1')
view1: Child2Component;//獲得子組件之后就可以調(diào)用子組件中的方法了
constructor() {
}
ngOnInit(): void {
setInterval(() => {
this.view1.greeting('XiaoMing');
}, 5000);
}
//在組件模板的內(nèi)容都已經(jīng)呈現(xiàn)給用戶看之后懒震,會(huì)調(diào)用這兩個(gè)方法
ngAfterViewInit(): void {
console.log('父組件的視圖初始化完畢');
}
ngAfterViewChecked(): void {
console.log('父組件的視圖變更檢測(cè)完畢');
}
}
運(yùn)行結(jié)果:
-
初始化ngAfterViewInit
的方法會(huì)在變更檢測(cè)ngAfterViewChecked
方法之前調(diào)用
。這兩個(gè)方法都是在視圖組裝完畢之后
被調(diào)用的嗤详。 - 初始化方法只會(huì)被
調(diào)用一次个扰。
- 如果有子組件,需要等
所有子組件視圖組裝完畢
之后才會(huì)觸發(fā)父組件的ngAfterViewInit葱色、ngAfterViewChecked
(這里處理了兩個(gè)子組件所以子組件調(diào)用了兩次)递宅。 - 不要在這兩個(gè)方法中去改變視圖中綁定的東西,如果想改變也要寫(xiě)在一個(gè)setTimeout里邊苍狰。
- 如果想實(shí)現(xiàn)
ngAfterViewChecked
這個(gè)鉤子刻蟹,方法一定要非常高效,非常輕量級(jí)穴翩,不然會(huì)引起性能問(wèn)題氨菇。
//修改app.component.html
<h1>主組件</h1>
<div>
<app-child2 #view1></app-child2>
<app-child2 #view1></app-child2>
<button (click)="view1.greeting('William')">點(diǎn)擊按鈕</button>
{{message}}
</div>
//修改app.component.ts
message:string;
ngAfterViewInit(): void {
console.log("父組件的視圖初始化完畢");
this.message="Hello";
}
運(yùn)行結(jié)果:
啟動(dòng)項(xiàng)目,這時(shí)候會(huì)報(bào)一個(gè)錯(cuò)
因?yàn)锳ngular規(guī)定禁止在一個(gè)視圖在組裝好之后再去更新這個(gè)視圖响牛。
而ngAfterViewInit玷禽、ngAfterViewChecked
這2個(gè)鉤子恰是在視圖組裝好之后被觸發(fā)的赫段。
//解決辦法
ngAfterViewInit(): void {
console.log('父組件的視圖初始化完畢');
setTimeout(() => {
this.message = 'Hello';
}, 0);
}
讓其在JavaScript的另一個(gè)運(yùn)行周期中去運(yùn)行。
4.4 ngContent指令
投影矢赁,
在某些情況下糯笙,需要動(dòng)態(tài)改變模板的內(nèi)容
,可以用路由撩银,但路由
是一個(gè)相對(duì)比較麻煩
的東西给涕,而我要實(shí)現(xiàn)的功能沒(méi)有那么復(fù)雜,额获,沒(méi)有什么業(yè)務(wù)邏輯够庙,也不需要重用。
這個(gè)時(shí)候可以用投影抄邀≡耪#可以用ngContent將父組件
中任意片段投影到子組件中。
代碼示例:
//新建組件child3
ng g component child3
//child3.component.html
<div class="wrapper">
<h2>我是子組件</h2>
<div>這個(gè)div定義在子組件中</div>
<ng-content></ng-content>
</div>
///child3.component.css
.wrapper{
background:deepskyblue;
}
//app.component.html
<div class="wrapper">
<h2>我是父組件</h2>
<div>這個(gè)div定義在父組件中</div>
<app-child3>
<div>這個(gè)div是父組件投影到子組件的</div>
</app-child3>
</div>
//app.component.css
.wrapper{
background:#ff6700;
}
運(yùn)行結(jié)果:
一個(gè)組件可以在其模板中聲明多個(gè)ng-content標(biāo)簽
假設(shè)子組件的部分是由三部分組成的境肾,頁(yè)頭剔难,頁(yè)腳和內(nèi)容區(qū)。頁(yè)頭和頁(yè)腳由父組件投影進(jìn)來(lái)奥喻,內(nèi)容區(qū)自己定義
//修改child3.component.html
<div class="wrapper">
<h2>我是子組件</h2>
<ng-content select=".header"></ng-content>
<div>這個(gè)div定義在子組件中</div>
<ng-content select=".footer"></ng-content>
</div>
//修改app.component.html
<div class="wrapper">
<h2>我是父組件</h2>
<div>這個(gè)div定義在父組件中</div>
<app-child3>
<div class="header">這是頁(yè)頭.這個(gè)div是父組件投影到子組件的,title是{{title}}</div>
<div class="footer">這是頁(yè)腳.這個(gè)div是父組件投影到子組件的</div>
</app-child3>
</div>
//app.component.ts
export class AppComponent {
title = 'XiaoMing';
}
運(yùn)行結(jié)果:
使用的{{title}}
只能綁定父組件中的屬性偶宫。
Angular還可以用屬性綁定
的形式很方便的插入一段HTML。
//修改app.component.html
<div class="wrapper">
<h2>我是父組件</h2>
<div>這個(gè)div定義在父組件中</div>
<app-child3>
<div class="header">這是頁(yè)頭.這個(gè)div是父組件投影到子組件的,title是{{title}}</div>
<div class="footer">這是頁(yè)腳.這個(gè)div是父組件投影到子組件的</div>
</app-child3>
</div>
<div [innerHTML]="htmlContent"></div>
//修改app.component.ts
export class AppComponent {
title = 'XiaoMing';
htmlContent = '<p>this are some strings.</p>';
}
innerHTML
這種方式只能在瀏覽器
中使用环鲤,而ngContent是跨平臺(tái)的纯趋,
可以在app應(yīng)用中使用ngContent
可以設(shè)置多個(gè)投影點(diǎn)。
動(dòng)態(tài)生成一段HTML楔绞,應(yīng)該
優(yōu)先考慮ngContent
這種方式结闸。
4.5 ngAfterContentInit和ngAfterContentChecked
//修改app.component.ts
import {AfterContentChecked, AfterContentInit, AfterViewInit, Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements AfterContentInit, AfterContentChecked, AfterViewInit {
title = 'XiaoMing';
htmlContent = '<p>this are some strings.</p>';
ngAfterContentInit(): void {
console.log('父組件投影內(nèi)容初始化完畢');
}
ngAfterContentChecked(): void {
console.log('父組件投影內(nèi)容變更檢測(cè)完畢');
}
ngAfterViewInit(): void {
console.log('父組件視圖內(nèi)容初始化完畢');
}
}
//修改child3.component.ts
import {AfterContentChecked, AfterContentInit, Component, OnInit} from '@angular/core';
@Component({
selector: 'app-child3',
templateUrl: './child3.component.html',
styleUrls: ['./child3.component.css']
})
export class Child3Component implements OnInit, AfterContentInit, AfterContentChecked {
constructor() {
}
ngOnInit() {
}
ngAfterContentChecked(): void {
console.log('子組件投影內(nèi)容變更檢測(cè)完畢');
}
ngAfterContentInit(): void {
console.log('子組件投影內(nèi)容初始化完畢');
}
}
運(yùn)行結(jié)果: