一、生命周期钩子
每个组件都有一个被 Angular 管理的生命周期。
Angular 创建它,渲染它,创建并渲染它的子组件,在它被绑定的属性发生变化>时检查它,并在它从 DOM 中被移除前销毁它。
Angular 提供了生命周期钩子,把这些关键生命时刻暴露出来,赋予你在它们发生时采取行动的能力。
除了那些组件内容和视图相关的钩子外,指令有相同生命周期钩子。
二、组件生命周期钩子概览
指令和组件的实例有一个生命周期:新建、更新和销毁。 通过实现一个或多个 Angular core
库里定义的生命周期钩子接口,开发者可以介入该生命周期中的这些关键时刻。
每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上 ng
前缀构成的。比如,OnInit
接口的钩子方法叫做 ngOnInit
, Angular 在创建组件后立刻调用它,
没有指令或者组件会实现所有这些接口,并且有些钩子只对组件有意义。只有在指令/组件中定义过的那些钩子方法才会被 Angular 调用。
三、生命周期的顺序
1.组件生命周期概览
红色的被调用一次,绿色的会被调用多次。
这里分为了三个阶段,
组件初始化阶段
,变化检测
,组件销毁
。会在组件初始化后看到组件,在变化检测阶段让属性值和页面展示保持一致。
变化检测中的四个方法和组件初始化中的四个方法是一样的。
一共只有9个方法。
当 Angular 使用构造函数新建一个组件或指令后,就会按下面的顺序在特定时刻调用这些生命周期钩子方法:
钩子 | 用途及时机 |
---|---|
ngOnChanges() |
当 Angular(重新)设置数据绑定输入属性时响应。 该方法接受当前和上一属性值的 SimpleChanges 对象当被绑定的输入属性的值发生变化时调用,首次调用一定会发生在 ngOnInit() 之前。 |
ngOnInit() |
在 Angular 第一次显示数据绑定和设置指令/组件的输入属性之后,初始化指令/组件。在第一轮 ngOnChanges() 完成之后调用,只调用一次。 |
ngDoCheck() |
检测,并在发生 Angular 无法或不愿意自己检测的变化时作出反应。在每个 Angular 变更检测周期中调用,ngOnChanges() 和 ngOnInit() 之后。 |
ngAfterContentInit() |
当把内容投影进组件之后调用。第一次 ngDoCheck() 之后调用,只调用一次。 |
ngAfterContentChecked() |
每次完成被投影组件内容的变更检测之后调用。ngAfterContentInit() 和每次 ngDoCheck() 之后调用 |
ngAfterViewInit() |
初始化完组件视图及其子视图之后调用。第一次 ngAfterContentChecked() 之后调用,只调用一次。 |
ngAfterViewChecked() |
每次做完组件视图和子视图的变更检测之后调用。ngAfterViewInit() 和每次 ngAfterContentChecked() 之后调用。 |
ngOnDestroy() |
当 Angular 每次销毁指令/组件之前调用并清扫。 在这儿反订阅可观察对象和分离事件处理器,以防内存泄漏。在 Angular 销毁指令/组件之前调用。 |
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');
}
}
调用顺序如图所示
首先会调用构造函数
ngOnChanges:当一个父组件修改或初始化一个子组件的`输入属性`的时候被调用
ngOnInit:初始化(如果初始化的逻辑需要依赖输入属性,那就一定要写在ngOnInit中,而不要写在构造函数中)
ngDoCheck:用来检测
ngAfterContentInit:
ngAfterContentChecked:
ngAfterViewInit
ngAfterViewChecked
ngDoCheck
ngAfterContentChecked
ngAfterViewChecked
四、具体钩子
4.1 ngOnchanges
当
父组件初始化
或修改子组件的输入参数
时会被调用。
可变对象,不可变对象
字符串是不可变的(改变值只是修改了内存中的指向)
对象的值是可变的
代码示例
新建child组件
ng g component child
//child.component.html
<div style="background: deepskyblue;">
<h2>我是子组件</h2>
<div>问候语:{{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>
问候语:<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'};
}
运行结果:
改变父组件‘问候语’的值为'Hi!'
但当我们改变父组件中'姓名'的值为'XiaoMing11111'时,控制台不会打印出新东西。
因为greeting是字符串是不可变对象
(每次值改变的时候都会创建一个新的字符串,然后把引用指向新的字符串),而user是可变对象
,修改姓名的值的时候并没有改变user对象的引用。那么怎么监控可变对象呢,用doCheck
当我改变页面中子组件的消息时,也不会打印出新东西,因为子组件中的message属性没有被@Input()标记不是输入属性。
4.2 变更检测机制和DoCheck钩子
查看package.json文件中的dependencies的zone.js
就是zone.js
来实现变更检测机制
的,主要作用是保证属性的变化和页面的变化是一致的
,浏览器中发生的任何异步事件都会触发变更检测,比如点击按钮,输入数据...
默认的是
Default
策略,当父组件变化时会检测整个组件树
如果在子组件中设置了
OnPush
,当父组件变化时就只会检测父组件
。
当孙子组件1发生变化后,红色的部分都会被检测一遍,也就是调DoCheck方法。并且是从父组件开始检查。
书接上文,我们在child组件中添加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 + '变为' + this.user.name);
this.OldUserName = this.user.name;
}
if (this.changeDetected) {
this.noChangeCount = 0;
} else {
this.noChangeCount += 1;
console.log('DoCheck:user.name没变化时ngDoCheck方法已经被调用' + this.noChangeCount + '次');
}
this.changeDetected = false;
}
}
DoCheck运行效果:
- 页面初始化时
docheck
调用一次
- 当我在问候语的输入框和姓名的输入框中来回切换点击的时候,就会触发
docheck
方法。当我改变姓名的值的时候也会触发。之后我再点击输入框的时候,调用次数重置为1。这就是上一段代码要实现的效果。
虽然当我修改姓名的时候这个钩子会被调用,但是我们必须要小心ngdocheck这个钩子会
非常频繁
的被调用,每一次变化都会被调用,在这个例子中,我还没做任何操作呢,只是在页面随便点点就会被调用好几次,只有很少的调用次数是修改数据的时候触发的。所以对ngDoCheck这个方法的实现一定要
非常高效
,非常轻量级,不然会引起性能问题。不光是这个方法,变更检测中的那些带Check
的方法都应该这样
4.3 view钩子
如何在父组件
中调用子组件
中的方法
新建一个子组件
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')">点击按钮</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;//获得子组件之后就可以调用子组件中的方法了
constructor() {}
ngOnInit(): void {
this.view1.greeting('XiaoMing');//调用方法
}
}
运行效果:
会在控制台打印出HelloXiaoMing
然后我点击按钮,会打印出HelloWilliam
这样就实现了在父组件中调用子组件方法。
现在学习那两个钩子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('子组件的视图变更检测完毕');
}
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;//获得子组件之后就可以调用子组件中的方法了
constructor() {
}
ngOnInit(): void {
setInterval(() => {
this.view1.greeting('XiaoMing');
}, 5000);
}
//在组件模板的内容都已经呈现给用户看之后,会调用这两个方法
ngAfterViewInit(): void {
console.log('父组件的视图初始化完毕');
}
ngAfterViewChecked(): void {
console.log('父组件的视图变更检测完毕');
}
}
运行结果:
-
初始化ngAfterViewInit
的方法会在变更检测ngAfterViewChecked
方法之前调用
。这两个方法都是在视图组装完毕之后
被调用的。 - 初始化方法只会被
调用一次。
- 如果有子组件,需要等
所有子组件视图组装完毕
之后才会触发父组件的ngAfterViewInit、ngAfterViewChecked
(这里处理了两个子组件所以子组件调用了两次)。 - 不要在这两个方法中去改变视图中绑定的东西,如果想改变也要写在一个setTimeout里边。
- 如果想实现
ngAfterViewChecked
这个钩子,方法一定要非常高效,非常轻量级,不然会引起性能问题。
//修改app.component.html
<h1>主组件</h1>
<div>
<app-child2 #view1></app-child2>
<app-child2 #view1></app-child2>
<button (click)="view1.greeting('William')">点击按钮</button>
{{message}}
</div>
//修改app.component.ts
message:string;
ngAfterViewInit(): void {
console.log("父组件的视图初始化完毕");
this.message="Hello";
}
运行结果:
启动项目,这时候会报一个错
因为Angular规定禁止在一个视图在组装好之后再去更新这个视图。
而ngAfterViewInit、ngAfterViewChecked
这2个钩子恰是在视图组装好之后被触发的。
//解决办法
ngAfterViewInit(): void {
console.log('父组件的视图初始化完毕');
setTimeout(() => {
this.message = 'Hello';
}, 0);
}
让其在JavaScript的另一个运行周期中去运行。
4.4 ngContent指令
投影,
在某些情况下,需要动态改变模板的内容
,可以用路由,但路由
是一个相对比较麻烦
的东西,而我要实现的功能没有那么复杂,,没有什么业务逻辑,也不需要重用。
这个时候可以用投影。可以用ngContent将父组件
中任意片段投影到子组件中。
代码示例:
//新建组件child3
ng g component child3
//child3.component.html
<div class="wrapper">
<h2>我是子组件</h2>
<div>这个div定义在子组件中</div>
<ng-content></ng-content>
</div>
///child3.component.css
.wrapper{
background:deepskyblue;
}
//app.component.html
<div class="wrapper">
<h2>我是父组件</h2>
<div>这个div定义在父组件中</div>
<app-child3>
<div>这个div是父组件投影到子组件的</div>
</app-child3>
</div>
//app.component.css
.wrapper{
background:#ff6700;
}
运行结果:
一个组件可以在其模板中声明多个ng-content标签
假设子组件的部分是由三部分组成的,页头,页脚和内容区。页头和页脚由父组件投影进来,内容区自己定义
//修改child3.component.html
<div class="wrapper">
<h2>我是子组件</h2>
<ng-content select=".header"></ng-content>
<div>这个div定义在子组件中</div>
<ng-content select=".footer"></ng-content>
</div>
//修改app.component.html
<div class="wrapper">
<h2>我是父组件</h2>
<div>这个div定义在父组件中</div>
<app-child3>
<div class="header">这是页头.这个div是父组件投影到子组件的,title是{{title}}</div>
<div class="footer">这是页脚.这个div是父组件投影到子组件的</div>
</app-child3>
</div>
//app.component.ts
export class AppComponent {
title = 'XiaoMing';
}
运行结果:
使用的{{title}}
只能绑定父组件中的属性。
Angular还可以用属性绑定
的形式很方便的插入一段HTML。
//修改app.component.html
<div class="wrapper">
<h2>我是父组件</h2>
<div>这个div定义在父组件中</div>
<app-child3>
<div class="header">这是页头.这个div是父组件投影到子组件的,title是{{title}}</div>
<div class="footer">这是页脚.这个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是跨平台的,
可以在app应用中使用ngContent
可以设置多个投影点。
动态生成一段HTML,应该
优先考虑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('父组件投影内容初始化完毕');
}
ngAfterContentChecked(): void {
console.log('父组件投影内容变更检测完毕');
}
ngAfterViewInit(): void {
console.log('父组件视图内容初始化完毕');
}
}
//修改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('子组件投影内容变更检测完毕');
}
ngAfterContentInit(): void {
console.log('子组件投影内容初始化完毕');
}
}
运行结果: