Angular25 组件的生命周期钩子
1 生命周期钩子概述
组件共有9个生命周期钩子
1.1 生命周期的执行顺序
技巧01:测试时父组件传递对子组件的输入属性进行初始化操作
import { Component, Input, SimpleChanges, OnInit, OnChanges, DoCheck, AfterContentChecked, AfterViewInit, AfterContentInit, AfterViewChecked, OnDestroy } from '@angular/core'; let logIndex : number = 1; @Component({ selector: 'life', templateUrl: './life.component.html', styleUrls: ['./life.component.scss'] }) export class LifeComponent implements OnInit, OnChanges, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy { @Input() name : string; logIt(msg : String) { console.log(`#${logIndex++} - ${msg}`); } constructor() { this.logIt("name属性在constructor里面的值为: " + this.name); } ngOnInit() { this.logIt("ngOnInit"); } ngOnChanges(changes : SimpleChanges){ this.logIt("name属性在ngOnChanges里面的值为: " + this.name); } ngDoCheck() { this.logIt("ngDoCheck"); } ngAfterContentInit() { this.logIt("ngAfterContentInit"); } ngAfterContentChecked() { this.logIt("ngAfterContentChecked"); } ngAfterViewInit() { this.logIt("ngAfterViewInit"); } ngAfterViewChecked() { this.logIt("ngAfterViewChecked"); } ngOnDestroy() { this.logIt("ngOnDestroy"); } }
2 ngOnChanges
2.1 可变对象和不可变对象
在JavaScript中string类型是不可变对象,自定义的对象是可变对象;例如:
var a = "hello"; -> 变量a中存储的是字符串“hello”对应的内存地址
a = "warrior"; -> 变量a中存储的是字符串“warrior”对应的内存地址
结论:将不同的字符串赋值给同一个变量时,变量的值会发生改变;所以string类型是不可变类型【PS: 跟Java一样】
var user : {name : string} = {name : "warrior"}; -> 变量user中存储的是对象{name : "warrior"}对应的内存地址
user.name = "fury"; -> 变量user中还是存储的对象{name : "fury”}对应的内存地址,而且{name : "warrior"}和{name : "fury”}是同一个对象,所以他们的内存地址相同,故变量user中存储的值没有发生改变,改变的仅仅是变量user所指向的那个对象中的内容而已
结论:修改一个对象的内容并不会改变这个对象的内存地址,所以对象是可变对象
2.2 ngOnChanges
当输入属性的值发生改变时就会触发ngOnChanges
技巧01:如果输入属性的类型是一个对象时,需要区分是可变对象还是不可变对象,只有不可变对象时才会触发ngOnChanges;总之记住只要输入属性的值发生改变才会触发ngOnChanges
技巧02:ngOnChanges方法需要传入一个 SimpleChanges 类型的参数,可以利用该参数来查看输入属性改变前后的值,以及是否是初次赋值
技巧03:JSON.stringfy() 方法在将数据转化成JSON时可以进行格式化,例如
ngOnChanges(simpleChanges : SimpleChanges) {
console.log(JSON.stringify(simpleChanges, null, 2));
}
2.2.1 子组件代码
<div class="panel panel-primary"> <div class="panel-heading">子组件</div> <div class="panel-body"> <p>问候语:{{greeting}}</p> <p>姓名:{{user.name}}</p> <p>年龄:{{age}}</p> <p> 消息:<input type="text" name="message" [(ngModel)]="message" /> </p> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, EventEmitter, Output, Input, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit, OnChanges {
@Input()
greeting : string;
@Input()
user : {name : string};
@Input()
age : number;
message : string;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
setInterval(() => {
this.currentDate = new Date();
}, 1000);
}
ngOnChanges(simpleChanges : SimpleChanges) {
console.log(JSON.stringify(simpleChanges, null, 2));
}
}
2.2.2 父组件代码
<div class="panel panel-primary"> <div class="panel-heading">父组件</div> <div class="panel-body"> <p> 问候语:<input type="text" name="greeting" [(ngModel)]="greeting" /> </p> <p> 姓名:<input type="text" name="name" [(ngModel)]="user.name" /> </p> <p> 年龄:<input type="number" [(ngModel)]="age" /> </p> <child [greeting]="greeting" [user]="user" [age]="age"></child> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit {
greeting : string = "Hello";
user : {name : string} = {name : "王杨帅"};
age : number = 8;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
setInterval(() => {
this.currentDate = new Date();
}, 1000);
}
}
2.2.3 效果图
3 ngDoCheck
3.1 变更检测机制
angular中由zone.js去负责监听浏览器中所有异步事件(事件[单击、双击]、定时器、ajax)来保证模板和组件属性的变化时同步的;
只要zone.js检测到有异步事件发生时所有监测机制是默认检测机制的组件都会触发ngDoCheck
应用:当输入属性的类型是可变对象时,即使可变对象的内容发生了变化 ngOnchanges 也不会被调用,但是 ngDoCheck 会被调用;所以我们可以利用 ngDoCheck 来检测可变对象的变化
坑01:zone.js检测到任何一个组件有异步事件发生都会让所有采用默认变更检测机制的组件执行 ngDoCheck,所以 ngDoCheck 方法要慎用,而且逻辑不能太复杂,不然会影响性能
3.2 变更检测策略
3.2.1 default
angular默认的变更检测策略
如果所有的组件都是默认的变更检测策略,那么当一个组件发生改变时angular会对所有的组件进行变更检查
3.2.2 onPush
待更新...
3.3 代码汇总
3.3.1 父组件代码
<div class="panel panel-primary"> <div class="panel-heading">父组件</div> <div class="panel-body"> <p> 问候语:<input type="text" name="greeting" [(ngModel)]="greeting" /> </p> <p> 姓名:<input type="text" name="name" [(ngModel)]="user.name" /> </p> <p> 年龄:<input type="number" [(ngModel)]="age" /> </p> <child [greeting]="greeting" [user]="user" [age]="age"></child> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit {
greeting : string = "Hello";
user : {name : string} = {name : "王杨帅"};
age : number = 8;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
}
}
3.3.2 子组件代码
<div class="panel panel-primary"> <div class="panel-heading">子组件</div> <div class="panel-body"> <p>问候语:{{greeting}}</p> <p>姓名:{{user.name}}</p> <p>年龄:{{age}}</p> <p> 消息:<input type="text" name="message" [(ngModel)]="message" /> </p> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, EventEmitter, Output, Input, OnChanges, SimpleChanges } from '@angular/core';
import { DoCheck } from '@angular/core/src/metadata/lifecycle_hooks';
@Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit, OnChanges, DoCheck {
@Input()
greeting : string;
@Input()
user : {name : string};
oldName : string; // 存储上一次的值
changeNum : number = 0; // 非本组件触发ngDoCheck方法的次数
changeDetected : boolean; // 是否是本组件触发ngDoCheck方法
@Input()
age : number;
message : string;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
}
ngOnChanges(simpleChanges : SimpleChanges) {
console.log(JSON.stringify(simpleChanges, null, 2));
}
ngDoCheck() : void {
// 如果检测到是本组件的输入属性变化
if (this.user.name !== this.oldName) {
this.changeDetected = true;
console.log("ngDoCheck: user.name 从 "+ this.oldName +" 变成了 "+ this.user.name +" ");
this.oldName = this.user.name;
}
if (this.changeDetected) {
this.changeNum = 0; // 本组件触发的ngDoCheck就进行清零操作
} else { // 非本组件触发的ngDoCheck方法就进行加一操作
this.changeNum = this.changeNum + 1;
console.log("ngDoCheck: user.name没有变化,ngDoCheck方法被调用了" + this.changeNum + "次");
}
this.changeDetected = false;
}
}
3.3.3 效果展示
4 ngAfterViewInit 和 ngAfterViewChecked
4.1 父组件调用子组件的方法
这两个钩子是在组件的模板已经完全组装好后才会被调用,所以在这两个钩子中不能修改和模板相关的数据信息
4.1.1 利用模板变量实现
在父组件的视图模板中为子组件添加一个模板变量,然后在父组件中就可以利用这个模板变量来调用子组件的方法了
4.1.2 利用子组件的实例实现
在父组件的视图模板中为子组件添加一个模板变量,然后在组件类中利用@ViewChild来实例化一个子组件实例,再通过这个实例在父组件类中调用子组件的方法
4.2 调用顺序
4.2.1 组件中这两个钩子的调用顺序
组件被加载 -> 视图初始化钩子
-> 视图变更检测钩子
-> 视图变更检测钩子
技巧01:组件被调用时先要执行视图初始化钩子,再执行视图变更检测钩子,而且视图初始化钩子只会被调用一次
4.2.2 父子组件中的调用顺序
父组件被加载 -> 子组件被加载 -> 子组件视图初始化钩子 -> 父组件视图初始化钩子
-> 子组件视图变更检测钩子 -> 父组件视图变更检测钩子
-> 子组件视图变更检测钩子 -> 父组件视图变更检测钩子
技巧01:先执行执行子父组件的视图初始化钩子 -> 再子父组件的视图变更检测钩子 -> 循环调用子父组件的视图变更检测钩子
技巧02:视图变更检测钩子的调用是先调用子组件的再调用父组件的
技巧03:视图初始化钩子的调用是先调用子组件的再调用父组件的
坑01:这两个钩子都是在视图组装好之后触发的
4.2.3 修改数据
视图初始化钩子和视图变更检测钩子中不能修改和视图相关的数据信息 -> 可以利用一个定时器来实现,因为利用定时器可以让操作在另外一个JavaScript运行周期中运行
4.2.4 代码汇总
》父组件
<div class="panel panel-primary"> <div class="panel-heading">父组件</div> <div class="panel-body"> <child #child1></child> <child #child2></child> <button (click)="child2.greeting('Zeus')">调用子组件child2的方法</button> <br /> <p>{{info}}</p> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChildComponent } from './child/child.component';
import { AfterViewInit, AfterContentChecked } from '@angular/core/src/metadata/lifecycle_hooks';
@Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit, AfterViewInit, AfterContentChecked {
@ViewChild("child1")
child1 : ChildComponent;
info : string;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
setInterval(() => {
this.child1.greeting("Warrior");
}, 5000);
}
ngAfterViewInit() : void {
console.log("父组件视图初始化完毕");
// this.info = "good boy"; // 视图初始化钩子中不能修改和视图相关的数据信息
// setTimeout(() => { // 上面一行代码的解决办法
// this.info = "王杨帅";
// }, 5000);
}
ngAfterContentChecked() : void {
console.log("父组件视图变更检测完毕")
this.info = "good boy"; // 视图变更检测钩子中可以修改和视图相关的数据信息
}
}
》子组件
<div class="panel panel-primary"> <div class="panel-heading">子组件</div> <div class="panel-body"> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, DoCheck, EventEmitter, Output, Input, OnChanges, SimpleChanges, AfterViewInit, AfterContentChecked } from '@angular/core';
@Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit, AfterViewInit, AfterContentChecked {
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
}
greeting(info : string) : void {
console.log("Hello " + info);
}
ngAfterViewInit() : void {
console.log("子组件视图初始化完毕");
}
ngAfterContentChecked() : void {
console.log("子组件视图变更检测完毕")
}
}
5 ngAfterContentInit 和 ngAfterContentChecked
5.1 内容投影
5.1.1 需求
如何将父组件的数据传递到子组件
5.1.2 方案
》利用路由解决
》利用子组件的输入属性解决
》利用服务解决
》利用投影解决 【推荐】
5.1.3 投影编程步骤
》子组件的视图
在子组件的视图中添加 ng-content 标签
》父组件视图
在父组件中添加子组件标签,并在子组件标签内编写需要进行投影的代码;当系统运行后会自动将需要投影的代码投影到子组件的 ng-content 标签里面
技巧01:ng-content 仅仅是一个占据位置的标签,系统运行后投影的内容会代替 ng-content 的位置
》如何实现多个投影
为子组件的每个 ng-content 标签添加 select 属性
技巧01: select 的属性值格式是 “.属性值”
在父组件视图中使用子组件的内部利用class为其指定投影到那个 ng-content
》代码汇总
<div class="panel panel-primary"> <div class="panel-heading">子组件</div> <div class="panel-body"> <div style="border: 1px solid red;"> <ng-content select=".red"></ng-content> </div> <hr /> <div style="border: 1px solid green"> <ng-content select=".green"></ng-content> </div> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
<div class="panel panel-primary"> <div class="panel-heading">父组件</div> <div class="panel-body"> <child> <div class="red"> 我是父组件传过来的数据【放到子组件的红框中】 </div> <div class="green"> 我是父组件传过来的数据02【放到子组件的绿框中】 </div> </child> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
5.2 执行顺序
5.2.1 同一组件中这两个钩子的调用顺序
先调用投影内容初始化钩子 -> 再调用投影内容变更检测钩子
5.2.2 父子组件中的这两个钩子的调用顺序
先调用父组件中的钩子 -> 再调用子组件的的钩子
技巧01:投影内容相关的两个钩子在父子组件中的调用顺序和视图相关的两个钩子在父子组件中的调用顺序恰恰相反
5.2.3 修改数据
投影相关的两个钩子可以修改和视图相关的数据信息
愿意:投影相关的两个钩子是在视图组装完毕之前调用的,而视图相关的两个钩子是在视图组装完毕后调用的
5.2.4 代码汇总
》父组件代码
<div class="panel panel-primary"> <div class="panel-heading">父组件</div> <div class="panel-body"> <child> <div class="red"> 我是父组件传过来的数据【放到子组件的红框中】 </div> <div class="green"> 我是父组件传过来的数据02【放到子组件的绿框中】 </div> </child> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, ViewChild } from '@angular/core';
import { ChildComponent } from './child/child.component';
import { AfterViewInit, AfterContentChecked, AfterContentInit, AfterViewChecked } from '@angular/core/src/metadata/lifecycle_hooks';
@Component({
selector: 'parent',
templateUrl: './parent.component.html',
styleUrls: ['./parent.component.scss']
})
export class ParentComponent implements OnInit,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked {
@ViewChild("child1")
child1 : ChildComponent;
currentDate : Date;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
}
ngAfterContentInit() : void {
console.log("父组件投影内容初始化完毕");
}
ngAfterContentChecked() : void {
console.log("父组件投影内容变更检测完毕");
}
ngAfterViewChecked() : void {
console.log("父组件视图内容变更检测完毕");
}
ngAfterViewInit() : void {
console.log("父组件视图初始化完毕");
}
}
》子组件代码
<div class="panel panel-primary"> <div class="panel-heading">子组件</div> <div class="panel-body"> <div style="border: 1px solid red;"> <ng-content select=".red"></ng-content> </div> <hr /> <div style="border: 1px solid green"> <ng-content select=".green"></ng-content> </div> <p>{{info}}</p> </div> <div class="panel-footer">{{currentDate | date : "yyyy-MM-dd HH:mm:ss"}}</div> </div>
import { Component, OnInit, DoCheck,AfterViewChecked, EventEmitter, AfterContentInit, Output, Input, OnChanges, SimpleChanges, AfterViewInit, AfterContentChecked } from '@angular/core';
@Component({
selector: 'child',
templateUrl: './child.component.html',
styleUrls: ['./child.component.scss']
})
export class ChildComponent implements OnInit,
AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked {
currentDate : Date;
info : string;
constructor() { }
ngOnInit() {
this.currentDate = new Date();
// setInterval(() => {
// this.currentDate = new Date();
// }, 1000);
}
ngAfterContentInit() : void {
console.log("子组件投影内容初始化完毕");
// this.info = "warrior"; // 投影内容初始化完毕时可以修改视图相关数据信息
}
ngAfterContentChecked() : void {
console.log("子组件投影内容变更检测完毕");
this.info = "fury"; // 投影内容变更检测完毕时可以修改视图相关数据信息
}
ngAfterViewChecked() : void {
console.log("子组件视图内容变更检测完毕");
// this.info = "warrior";
}
ngAfterViewInit() : void {
console.log("子组件视图初始化完毕");
}
}
5.2.5 效果展示