12.《Angular生命周期》

一、生命周期钩子

每个组件都有一个被 Angular 管理的生命周期。

Angular 创建它,渲染它,创建并渲染它的子组件,在它被绑定的属性发生变化>时检查它,并在它从 DOM 中被移除前销毁它。

Angular 提供了生命周期钩子,把这些关键生命时刻暴露出来,赋予你在它们发生时采取行动的能力。

除了那些组件内容和视图相关的钩子外,指令有相同生命周期钩子。

二、组件生命周期钩子概览

指令和组件的实例有一个生命周期:新建、更新和销毁。 通过实现一个或多个 Angular core 库里定义的生命周期钩子接口,开发者可以介入该生命周期中的这些关键时刻。

每个接口都有唯一的一个钩子方法,它们的名字是由接口名再加上 ng 前缀构成的。比如,OnInit接口的钩子方法叫做 ngOnInit, Angular 在创建组件后立刻调用它,

没有指令或者组件会实现所有这些接口,并且有些钩子只对组件有意义。只有在指令/组件中定义过的那些钩子方法才会被 Angular 调用。

三、生命周期的顺序

1.组件生命周期概览

组件生命周期.png
  • 红色的被调用一次,绿色的会被调用多次。

  • 这里分为了三个阶段,组件初始化阶段变化检测组件销毁

  • 会在组件初始化后看到组件,在变化检测阶段让属性值和页面展示保持一致。

  • 变化检测中的四个方法和组件初始化中的四个方法是一样的。

一共只有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');
  }
}

调用顺序如图所示


调用结果.png

首先会调用构造函数

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'};
}

运行结果:

结果1.png

改变父组件‘问候语’的值为'Hi!'
结果2.png

但当我们改变父组件中'姓名'的值为'XiaoMing11111'时,控制台不会打印出新东西。
结果3.png

因为greeting是字符串是不可变对象(每次值改变的时候都会创建一个新的字符串,然后把引用指向新的字符串),而user是可变对象,修改姓名的值的时候并没有改变user对象的引用。那么怎么监控可变对象呢,用doCheck

当我改变页面中子组件的消息时,也不会打印出新东西,因为子组件中的message属性没有被@Input()标记不是输入属性。


结果4.png

4.2 变更检测机制和DoCheck钩子

查看package.json文件中的dependencies的zone.js

就是zone.js来实现变更检测机制的,主要作用是保证属性的变化和页面的变化是一致的,浏览器中发生的任何异步事件都会触发变更检测,比如点击按钮,输入数据...

变更检测机制1.png
  • 默认的是Default策略,当父组件变化时会检测整个组件树

  • 如果在子组件中设置了OnPush,当父组件变化时就只会检测父组件

变更检测机制2.png

当孙子组件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运行效果1.png

  • 页面初始化时docheck调用一次
DoCheck运行效果2.png
  • 当我在问候语的输入框和姓名的输入框中来回切换点击的时候,就会触发docheck方法。当我改变姓名的值的时候也会触发。之后我再点击输入框的时候,调用次数重置为1。这就是上一段代码要实现的效果。
DoCheck运行效果3.png
  • 虽然当我修改姓名的时候这个钩子会被调用,但是我们必须要小心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');//调用方法
  }

}


运行效果:

效果1.png

  • 会在控制台打印出HelloXiaoMing

  • 然后我点击按钮,会打印出HelloWilliam

这样就实现了在父组件中调用子组件方法。

现在学习那两个钩子AfterViewInitAfterViewChecked

//修改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('父组件的视图变更检测完毕');
  }

}

运行结果:

运行结果1.png

  • 初始化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";
  }

运行结果:

结果2.png

启动项目,这时候会报一个错

因为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;
}

运行结果:

运行结果1.png

一个组件可以在其模板中声明多个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';
}

运行结果:

运行结果2.png

使用的{{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>';
}
运行结果3.png
  • 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('子组件投影内容初始化完毕');
  }
}

运行结果:

运行结果.png

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343