美文网首页前端修仙之路
12.《Angular生命周期》

12.《Angular生命周期》

作者: 笨蛋小明 | 来源:发表于2018-07-16 04:10 被阅读0次

一、生命周期钩子

每个组件都有一个被 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

相关文章

  • 12.《Angular生命周期》

    一、生命周期钩子 每个组件都有一个被 Angular 管理的生命周期。Angular 创建它,渲染它,创建并渲染它...

  • angular生命周期

    大纲 1、angular生命周期是什么2、生命周期钩子分类3、Angular 2 指令生命周期钩子的作用及调用顺序...

  • angular中的生命周期

    生命周期 1、Angular每个组件都存在一个生命周期,从创建,变更到销毁。Angular提供组件生命周期钩子,把...

  • angular6.x--生命周期

    按照生命周期执行的先后顺序,Angular生命周期接口如下所示 生命周期顺序简写在Angular通过构造函数创建组...

  • Angular 2+ 的组件生命周期

    参考资料: angular 2.0 从0到1 -> #Angular 2的组件生命周期

  • angular 钩子

    angular 钩子ngOnInit是 Angular 组件生命周期中的一个钩子,Angular 中的所有钩子和调...

  • Angular2生命周期钩子函数

    Angular每个组件都存在一个生命周期,从创建,变更到销毁。Angular提供组件生命周期钩子,把这些关键时刻暴...

  • Angular2生命周期钩子函数

    Angular每个组件都存在一个生命周期,从创建,变更到销毁。Angular提供组件生命周期钩子,把这些关键时刻暴...

  • Angular更新机制(一):Angular的生命周期

    Angular更新机制(一):Angular的生命周期 了解Angular的更新机制之前,首先需要了解Angula...

  • Angular生命周期解析

    每个组件都有一个被Angular管理的生命周期,Angular提供了生命周期钩子,把这些关键生命时刻暴露出来,赋予...

网友评论

    本文标题:12.《Angular生命周期》

    本文链接:https://www.haomeiwen.com/subject/wbvupftx.html