當(dāng) Angular 實(shí)例化組件類并渲染組件視圖及其子視圖時(shí),組件實(shí)例的生命周期就開(kāi)始了。生命周期一直伴隨著變更檢測(cè),Angular 會(huì)檢查數(shù)據(jù)綁定屬性何時(shí)發(fā)生變化,并按需更新視圖和組件實(shí)例。當(dāng) Angular 銷毀組件實(shí)例并從 DOM 中移除它渲染的模板時(shí),生命周期就結(jié)束了。當(dāng) Angular 在執(zhí)行過(guò)程中創(chuàng)建、更新和銷毀實(shí)例時(shí),指令就有了類似的生命周期。
你的應(yīng)用可以使用生命周期鉤子方法來(lái)觸發(fā)組件或指令生命周期中的關(guān)鍵事件,以初始化新實(shí)例,需要時(shí)啟動(dòng)變更檢測(cè),在變更檢測(cè)過(guò)程中響應(yīng)更新,并在刪除實(shí)例之前進(jìn)行清理。
在使用生命周期鉤子之前,你應(yīng)該對(duì)這些內(nèi)容有一個(gè)基本的了解:
可以通過(guò)實(shí)現(xiàn)一個(gè)或多個(gè) Angular ?core
?庫(kù)中定義的生命周期鉤子接口來(lái)響應(yīng)組件或指令生命周期中的事件。這些鉤子讓你有機(jī)會(huì)在適當(dāng)?shù)臅r(shí)候?qū)M件或指令實(shí)例進(jìn)行操作,比如 Angular 創(chuàng)建、更新或銷毀這個(gè)實(shí)例時(shí)。
每個(gè)接口都有唯一的一個(gè)鉤子方法,它們的名字是由接口名再加上 ?ng
?前綴構(gòu)成的。比如,?OnInit
?接口的鉤子方法叫做 ?ngOnInit()
?。如果你在組件或指令類中實(shí)現(xiàn)了這個(gè)方法,Angular 就會(huì)在首次檢查完組件或指令的輸入屬性后,緊接著調(diào)用它。
@Directive({selector: '[appPeekABoo]'})
export class PeekABooDirective implements OnInit {
constructor(private logger: LoggerService) { }
// implement OnInit's `ngOnInit` method
ngOnInit() {
this.logIt('OnInit');
}
logIt(msg: string) {
this.logger.log(`#${nextId++} ${msg}`);
}
}
你不必實(shí)現(xiàn)所有生命周期鉤子,只要實(shí)現(xiàn)你需要的那些就可以了。
當(dāng)你的應(yīng)用通過(guò)調(diào)用構(gòu)造函數(shù)來(lái)實(shí)例化一個(gè)組件或指令時(shí),Angular 就會(huì)調(diào)用那個(gè)在該實(shí)例生命周期的適當(dāng)位置實(shí)現(xiàn)了的那些鉤子方法。
Angular 會(huì)按以下順序執(zhí)行鉤子方法。可以用它來(lái)執(zhí)行以下類型的操作。
鉤子方法 |
用途 |
時(shí)機(jī) |
---|---|---|
|
當(dāng) Angular 設(shè)置或重新設(shè)置數(shù)據(jù)綁定的輸入屬性時(shí)響應(yīng)。 該方法接受當(dāng)前和上一屬性值的 注意,這發(fā)生的非常頻繁,所以你在這里執(zhí)行的任何操作都會(huì)顯著影響性能。 |
如果組件綁定過(guò)輸入屬性,那么在 注意,如果你的組件沒(méi)有輸入屬性,或者你使用它時(shí)沒(méi)有提供任何輸入屬性,那么框架就不會(huì)調(diào)用 |
|
在 Angular 第一次顯示數(shù)據(jù)綁定和設(shè)置指令/組件的輸入屬性之后,初始化指令/組件。 |
在第一輪 |
|
檢測(cè),并在發(fā)生 Angular 無(wú)法或不愿意自己檢測(cè)的變化時(shí)作出反應(yīng)。 |
緊跟在每次執(zhí)行變更檢測(cè)時(shí)的 |
|
當(dāng) Angular 把外部?jī)?nèi)容投影進(jìn)組件視圖或指令所在的視圖之后調(diào)用。 |
第一次 |
|
每當(dāng) Angular 檢查完被投影到組件或指令中的內(nèi)容之后調(diào)用。 |
|
|
當(dāng) Angular 初始化完組件視圖及其子視圖或包含該指令的視圖之后調(diào)用。 |
第一次 |
|
每當(dāng) Angular 做完組件視圖和子視圖或包含該指令的視圖的變更檢測(cè)之后調(diào)用。 |
|
|
每當(dāng) Angular 每次銷毀指令/組件之前調(diào)用并清掃。 在這兒反訂閱可觀察對(duì)象和分離事件處理器,以防內(nèi)存泄漏。 |
在 Angular 銷毀指令或組件之前立即調(diào)用。 |
現(xiàn)場(chǎng)演練 / 下載范例通過(guò)在受控于根組件 ?AppComponent
?的一些組件上進(jìn)行的一系列練習(xí),演示了生命周期鉤子的運(yùn)作方式。 每一個(gè)例子中,父組件都扮演了子組件測(cè)試臺(tái)的角色,以展示出一個(gè)或多個(gè)生命周期鉤子方法。
下表列出了這些練習(xí)及其簡(jiǎn)介。 范例代碼也用來(lái)闡明后續(xù)各節(jié)的一些特定任務(wù)。
組件 |
說(shuō)明 |
---|---|
? |
展示每個(gè)生命周期鉤子,每個(gè)鉤子方法都會(huì)在屏幕上顯示一條日志。 |
? |
展示了如何在自定義指令中使用生命周期鉤子。 |
? |
演示了每當(dāng)組件的輸入屬性之一發(fā)生變化時(shí),Angular 如何調(diào)用 |
? |
實(shí)現(xiàn)了一個(gè) |
? |
顯示 Angular 中的視圖所指的是什么。 演示了 |
? |
展示如何把外部?jī)?nèi)容投影進(jìn)組件中,以及如何區(qū)分“投影進(jìn)來(lái)的內(nèi)容”和“組件的子視圖”。 演示了 |
? |
演示了一個(gè)組件和一個(gè)指令的組合,它們各自有自己的鉤子。 |
使用 ?ngOnInit()
? 方法執(zhí)行以下初始化任務(wù)。
?ngOnInit()
? 是組件獲取初始數(shù)據(jù)的好地方。
請(qǐng)記住,只有在構(gòu)造完成之后才會(huì)設(shè)置指令的數(shù)據(jù)綁定輸入屬性。如果要根據(jù)這些屬性對(duì)指令進(jìn)行初始化,請(qǐng)?jiān)谶\(yùn)行 ?ngOnInit()
? 時(shí)設(shè)置它們。
?
ngOnChanges()
? 方法是你能訪問(wèn)這些屬性的第一次機(jī)會(huì)。Angular 會(huì)在調(diào)用 ?ngOnInit()
? 之前調(diào)用 ?ngOnChanges()
?,而且之后還會(huì)調(diào)用多次。但它只調(diào)用一次 ?ngOnInit()
?。
把清理邏輯放進(jìn) ?ngOnDestroy()
? 中,這個(gè)邏輯就必然會(huì)在 Angular 銷毀該指令之前運(yùn)行。
這里是釋放資源的地方,這些資源不會(huì)自動(dòng)被垃圾回收。如果你不這樣做,就存在內(nèi)存泄漏的風(fēng)險(xiǎn)。
?ngOnDestroy()
? 方法也可以用來(lái)通知應(yīng)用程序的其它部分,該組件即將消失。
下面的例子展示了各個(gè)生命周期事件的調(diào)用順序和相對(duì)頻率,以及如何在組件和指令中單獨(dú)使用或同時(shí)使用這些鉤子。
為了展示 Angular 如何以預(yù)期的順序調(diào)用鉤子,?PeekABooComponent
?演示了一個(gè)組件中的所有鉤子。
實(shí)際上,你很少會(huì)(幾乎永遠(yuǎn)不會(huì))像這個(gè)演示中一樣實(shí)現(xiàn)所有這些接口。
下列快照反映了用戶單擊 Create... 按鈕,然后單擊 Destroy... 按鈕后的日志狀態(tài)。
日志信息的日志和所規(guī)定的鉤子調(diào)用順序是一致的: ?OnChanges
?、?OnInit
?、?DoCheck
?(3x)、?AfterContentInit
?、?AfterContentChecked
?(3x)、 ?AfterViewInit
?、?AfterViewChecked
?(3x)和 ?OnDestroy
?
注意,該日志確認(rèn)了在創(chuàng)建期間那些輸入屬性(這里是 ?
name
?屬性)沒(méi)有被賦值。 這些輸入屬性要等到 ?onInit()
? 中才可用,以便做進(jìn)一步的初始化。
如果用戶點(diǎn)擊Update Hero按鈕,就會(huì)看到另一個(gè) ?OnChanges
?和至少兩組 ?DoCheck
?、?AfterContentChecked
?和 ?AfterViewChecked
?鉤子。 注意,這三種鉤子被觸發(fā)了很多次,所以讓它們的邏輯盡可能保持精簡(jiǎn)是非常重要的!
這個(gè) ?Spy
?例子演示了如何在指令和組件中使用鉤子方法。?SpyDirective
?實(shí)現(xiàn)了兩個(gè)鉤子 ?ngOnInit()
? 和 ?ngOnDestroy()
?,以便發(fā)現(xiàn)被監(jiān)視的元素什么時(shí)候位于當(dāng)前視圖中。
這個(gè)模板將 ?SpyDirective
?應(yīng)用到由父組件 ?SpyComponent
?管理的 ?ngFor
?內(nèi)的 ?<div>
? 中。
該例子不執(zhí)行任何初始化或清理工作。它只是通過(guò)記錄指令本身的實(shí)例化時(shí)間和銷毀時(shí)間來(lái)跟蹤元素在視圖中的出現(xiàn)和消失。
像這樣的間諜指令可以深入了解你無(wú)法直接修改的 DOM 對(duì)象。你無(wú)法觸及內(nèi)置 ?<div>
? 的實(shí)現(xiàn),也無(wú)法修改第三方組件,但是可以用指令來(lái)監(jiān)視這些元素。
這個(gè)指令定義了 ?ngOnInit()
? 和 ?ngOnDestroy()
? 鉤子,它通過(guò)一個(gè)注入進(jìn)來(lái)的 ?LoggerService
?把消息記錄到父組件中去。
let nextId = 1;
// Spy on any element to which it is applied.
// Usage: <div appSpy>...</div>
@Directive({selector: '[appSpy]'})
export class SpyDirective implements OnInit, OnDestroy {
private id = nextId++;
constructor(private logger: LoggerService) { }
ngOnInit() {
this.logger.log(`Spy #${this.id} onInit`);
}
ngOnDestroy() {
this.logger.log(`Spy #${this.id} onDestroy`);
}
}
你可以把這個(gè)偵探指令寫到任何內(nèi)置元素或組件元素上,以觀察它何時(shí)被初始化和銷毀。 下面是把它附加到用來(lái)重復(fù)顯示英雄數(shù)據(jù)的這個(gè) ?<div>
? 上。
<p *ngFor="let hero of heroes" appSpy>
{{hero}}
</p>
每個(gè)“偵探”的創(chuàng)建和銷毀都可以標(biāo)出英雄所在的那個(gè) ?<div>
? 的出現(xiàn)和消失。 添加一個(gè)英雄就會(huì)產(chǎn)生一個(gè)新的英雄 ?<div>
?。偵探的 ?ngOnInit()
? 記錄下了這個(gè)事件。
Reset 按鈕清除了這個(gè) ?heroes
?列表。 Angular 從 DOM 中移除了所有英雄的 div,并且同時(shí)銷毀了附加在這些 div 上的偵探指令。 偵探的 ?ngOnDestroy()
? 方法匯報(bào)了它自己的臨終時(shí)刻。
在這個(gè)例子中,?CounterComponent
?使用了 ?ngOnChanges()
? 方法,以便在每次父組件遞增其輸入屬性 ?counter
?時(shí)記錄一次變更。
這個(gè)例子將前例中的 ?SpyDirective
?用于 ?CounterComponent
?的日志,以便監(jiān)視這些日志條目的創(chuàng)建和銷毀。
一旦檢測(cè)到該組件或指令的輸入屬性發(fā)生了變化,Angular 就會(huì)調(diào)用它的 ?ngOnChanges()
? 方法。 這個(gè) onChanges 范例通過(guò)監(jiān)控 ?OnChanges()
? 鉤子演示了這一點(diǎn)。
ngOnChanges(changes: SimpleChanges) {
for (const propName in changes) {
const chng = changes[propName];
const cur = JSON.stringify(chng.currentValue);
const prev = JSON.stringify(chng.previousValue);
this.changeLog.push(`${propName}: currentValue = ${cur}, previousValue = ${prev}`);
}
}
?ngOnChanges()
? 方法獲取了一個(gè)對(duì)象,它把每個(gè)發(fā)生變化的屬性名都映射到了一個(gè)?SimpleChange
?對(duì)象, 該對(duì)象中有屬性的當(dāng)前值和前一個(gè)值。這個(gè)鉤子會(huì)在這些發(fā)生了變化的屬性上進(jìn)行迭代,并記錄它們。
這個(gè)例子中的 ?OnChangesComponent
?組件有兩個(gè)輸入屬性:?hero
?和 ?power
?。
@Input() hero!: Hero;
@Input() power = '';
宿主 ?OnChangesParentComponent
?綁定了它們,就像這樣:
<on-changes [hero]="hero" [power]="power"></on-changes>
下面是此例子中的當(dāng)用戶做出更改時(shí)的操作演示:
日志條目把 power 屬性的變化顯示為字符串。但請(qǐng)注意,?ngOnChanges()
? 方法不會(huì)捕獲對(duì) ?hero.name
? 更改。這是因?yàn)橹挥挟?dāng)輸入屬性的值發(fā)生變化時(shí),Angular 才會(huì)調(diào)用該鉤子。在這種情況下,?hero
?是輸入屬性,?hero
?屬性的值是對(duì) hero 對(duì)象的引用 。當(dāng)它自己的 ?name
?屬性的值發(fā)生變化時(shí),對(duì)象引用并沒(méi)有改變。
當(dāng) Angular 在變更檢測(cè)期間遍歷視圖樹(shù)時(shí),需要確保子組件中的某個(gè)變更不會(huì)嘗試更改其父組件中的屬性。因?yàn)閱蜗驍?shù)據(jù)流的工作原理就是這樣的,這樣的更改將無(wú)法正常渲染。
如果你需要做一個(gè)與預(yù)期數(shù)據(jù)流反方向的修改,就必須觸發(fā)一個(gè)新的變更檢測(cè)周期,以允許渲染這種變更。這些例子說(shuō)明了如何安全地做出這些改變。
AfterView 例子展示了 ?AfterViewInit()
? 和 ?AfterViewChecked()
? 鉤子,Angular 會(huì)在每次創(chuàng)建了組件的子視圖后調(diào)用它們。
下面是一個(gè)子視圖,它用來(lái)把英雄的名字顯示在一個(gè) ?<input>
? 中:
@Component({
selector: 'app-child-view',
template: `
<label for="hero-name">Hero name: </label>
<input type="text" id="hero-name" [(ngModel)]="hero">
`
})
export class ChildViewComponent {
hero = 'Magneta';
}
?AfterViewComponent
?把這個(gè)子視圖顯示在它的模板中:
template: `
<div>child view begins</div>
<app-child-view></app-child-view>
<div>child view ends</div>
`
下列鉤子基于子視圖中的每一次數(shù)據(jù)變更采取行動(dòng),它只能通過(guò)帶?@ViewChild
?裝飾器的屬性來(lái)訪問(wèn)子視圖。
export class AfterViewComponent implements AfterViewChecked, AfterViewInit {
private prevHero = '';
// Query for a VIEW child of type `ChildViewComponent`
@ViewChild(ChildViewComponent) viewChild!: ChildViewComponent;
ngAfterViewInit() {
// viewChild is set after the view has been initialized
this.logIt('AfterViewInit');
this.doSomething();
}
ngAfterViewChecked() {
// viewChild is updated after the view has been checked
if (this.prevHero === this.viewChild.hero) {
this.logIt('AfterViewChecked (no change)');
} else {
this.prevHero = this.viewChild.hero;
this.logIt('AfterViewChecked');
this.doSomething();
}
}
// ...
}
在這個(gè)例子中,當(dāng)英雄名字超過(guò) 10 個(gè)字符時(shí),?doSomething()
? 方法會(huì)更新屏幕,但在更新 ?comment
?之前會(huì)等一個(gè)節(jié)拍(tick)。
// This surrogate for real business logic sets the `comment`
private doSomething() {
const c = this.viewChild.hero.length > 10 ? "That's a long name" : '';
if (c !== this.comment) {
// Wait a tick because the component's view has already been checked
this.logger.tick_then(() => this.comment = c);
}
}
在組件的視圖合成完之后,就會(huì)觸發(fā) ?AfterViewInit()
? 和 ?AfterViewChecked()
? 鉤子。如果你修改了這段代碼,讓這個(gè)鉤子立即修改該組件的數(shù)據(jù)綁定屬性 ?comment
?,你就會(huì)發(fā)現(xiàn) Angular 拋出一個(gè)錯(cuò)誤。
?LoggerService.tick_then()
? 語(yǔ)句把日志的更新工作推遲了一個(gè)瀏覽器 JavaScript 周期,也就觸發(fā)了一個(gè)新的變更檢測(cè)周期。
當(dāng)你運(yùn)行 AfterView 范例時(shí),請(qǐng)注意當(dāng)沒(méi)有發(fā)生任何需要注意的變化時(shí),Angular 仍然會(huì)頻繁的調(diào)用 ?AfterViewChecked()
?。 要非常小心你放到這些方法中的邏輯或計(jì)算量。
內(nèi)容投影是從組件外部導(dǎo)入 HTML 內(nèi)容,并把它插入在組件模板中指定位置上的一種途徑。 可以在目標(biāo)中通過(guò)查找下列結(jié)構(gòu)來(lái)認(rèn)出內(nèi)容投影。
<ng-content>
? 標(biāo)簽。AngularJS 的開(kāi)發(fā)者把這種技術(shù)叫做 ?
transclusion
?。
這個(gè) AfterContent 例子探索了 ?AfterContentInit()
? 和 ?AfterContentChecked()
? 鉤子。Angular 會(huì)在把外部?jī)?nèi)容投影進(jìn)該組件時(shí)調(diào)用它們。
這次不再通過(guò)模板來(lái)把子視圖包含進(jìn)來(lái),而是改為從 ?AfterContentComponent
?的父組件中導(dǎo)入它。下面是父組件的模板:
`<after-content>
<app-child></app-child>
</after-content>`
注意,?<app-child>
? 標(biāo)簽被包含在 ?<after-content>
? 標(biāo)簽中。 永遠(yuǎn)不要在組件標(biāo)簽的內(nèi)部放任何內(nèi)容 —— 除非你想把這些內(nèi)容投影進(jìn)這個(gè)組件中。
現(xiàn)在來(lái)看該組件的模板:
template: `
<div>projected content begins</div>
<ng-content></ng-content>
<div>projected content ends</div>
`
?<ng-content>
? 標(biāo)簽是外來(lái)內(nèi)容的占位符。 它告訴 Angular 在哪里插入這些外來(lái)內(nèi)容。 在這里,被投影進(jìn)去的內(nèi)容就是來(lái)自父組件的 ?<app-child>
? 標(biāo)簽。
AfterContent 鉤子和 AfterView 相似。關(guān)鍵的不同點(diǎn)是子組件的類型不同。
ViewChildren
?,這些子組件的元素標(biāo)簽會(huì)出現(xiàn)在該組件的模板里面。
ContentChildren
?,這些子組件被 Angular 投影進(jìn)該組件中。下列 AfterContent 鉤子基于子級(jí)內(nèi)容中值的變化而采取相應(yīng)的行動(dòng),它只能通過(guò)帶有?@ContentChild
?裝飾器的屬性來(lái)查詢到“子級(jí)內(nèi)容”。
export class AfterContentComponent implements AfterContentChecked, AfterContentInit {
private prevHero = '';
comment = '';
// Query for a CONTENT child of type `ChildComponent`
@ContentChild(ChildComponent) contentChild!: ChildComponent;
ngAfterContentInit() {
// contentChild is set after the content has been initialized
this.logIt('AfterContentInit');
this.doSomething();
}
ngAfterContentChecked() {
// contentChild is updated after the content has been checked
if (this.prevHero === this.contentChild.hero) {
this.logIt('AfterContentChecked (no change)');
} else {
this.prevHero = this.contentChild.hero;
this.logIt('AfterContentChecked');
this.doSomething();
}
}
// ...
}
不需要等待內(nèi)容更新
該組件的 ?doSomething()
? 方法會(huì)立即更新該組件的數(shù)據(jù)綁定屬性 ?comment
?。而無(wú)需延遲更新以確保正確渲染 。
Angular 在調(diào)用 AfterView 鉤子之前,就已調(diào)用完所有的 AfterContent 鉤子。 在完成該組件視圖的合成之前, Angular 就已經(jīng)完成了所投影內(nèi)容的合成工作。 ?AfterContent...
? 和 ?AfterView...
? 鉤子之間有一個(gè)小的時(shí)間窗,允許你修改宿主視圖。
要監(jiān)控 ?ngOnChanges()
? 無(wú)法捕獲的變更,你可以實(shí)現(xiàn)自己的變更檢查邏輯,比如 DoCheck 的例子。這個(gè)例子展示了你如何使用 ?ngDoCheck()
? 鉤子來(lái)檢測(cè)和處理 Angular 自己沒(méi)有捕捉到的變化。
DoCheck 范例使用下面的 ?ngDoCheck()
? 鉤子擴(kuò)展了 OnChanges 范例:
ngDoCheck() {
if (this.hero.name !== this.oldHeroName) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Hero name changed to "${this.hero.name}" from "${this.oldHeroName}"`);
this.oldHeroName = this.hero.name;
}
if (this.power !== this.oldPower) {
this.changeDetected = true;
this.changeLog.push(`DoCheck: Power changed to "${this.power}" from "${this.oldPower}"`);
this.oldPower = this.power;
}
if (this.changeDetected) {
this.noChangeCount = 0;
} else {
// log that hook was called when there was no relevant change.
const count = this.noChangeCount += 1;
const noChangeMsg = `DoCheck called ${count}x when no change to hero or power`;
if (count === 1) {
// add new "no change" message
this.changeLog.push(noChangeMsg);
} else {
// update last "no change" message
this.changeLog[this.changeLog.length - 1] = noChangeMsg;
}
}
this.changeDetected = false;
}
這段代碼會(huì)檢查某些感興趣的值,捕獲并把它們當(dāng)前的狀態(tài)和之前的進(jìn)行比較。當(dāng) ?hero
?或 ?power
?沒(méi)有實(shí)質(zhì)性變化時(shí),它就會(huì)在日志中寫一條特殊的信息,這樣你就能看到 ?DoCheck()
? 被調(diào)用的頻率。其結(jié)果很有啟發(fā)性。
雖然 ?ngDoCheck()
? 鉤子可以檢測(cè)出英雄的 ?name
?何時(shí)發(fā)生了變化,但卻非常昂貴。無(wú)論變化發(fā)生在何處,每個(gè)變化檢測(cè)周期都會(huì)以很大的頻率調(diào)用這個(gè)鉤子。在用戶可以執(zhí)行任何操作之前,本例中已經(jīng)調(diào)用了20多次。
這些初始化檢查大部分都是由 Angular 首次在頁(yè)面的其它地方渲染不相關(guān)的數(shù)據(jù)觸發(fā)的。只要把光標(biāo)移動(dòng)到另一個(gè) ?<input>
? 就會(huì)觸發(fā)一次調(diào)用。其中的少數(shù)調(diào)用揭示了相關(guān)數(shù)據(jù)的實(shí)際變化情況。如果使用這個(gè)鉤子,那么你的實(shí)現(xiàn)必須非常輕量級(jí),否則會(huì)損害用戶體驗(yàn)。
更多建議: