Angular 2(七): 生命周期钩子

钩子的调用顺序:

consttructor -> ngOnChanges -> ngOnInit -> ngDoCheck ->

ngAfterContentInit -> ngAfterContentChecked -> ngAfterViewInit ->

ngAfterViewChecked -> ngOnDestroy

ngOnchanges

  • 当父组件初始化或者修改子组件的输入属性时调用;
  • 如果一个组件没有输入属性,那么永远不会调用该钩子;
  • 首次调用一定在ngOnInit之前;
  • 可以被多次调用;
    可变对象和不可变对象
    1
    2
    3
    let val = 'hello';
    val = 'hello world';
    `
  • ‘hello’在内存中被创建后赋值给val;
  • ‘hello world’在内存中被创建后赋值给val;

无论val如何变化,’hello’在内存中的地址始终保持不变。字符串是不可变对象。

1
2
let user = { name:'roy' };
user.name = 'joy';
  • 在内存中创建了user,name属性指向’roy’所在的地址;
  • 在内存中创建了’joy’,地址赋值给了user的属性name;
  • name发生了变化,但是user的地址始终不变;也就是说由于属性的改变,user(对象)是可变对象;

ngOnChanges触发机制

父组件
1
2
3
4
5
6
7
8
9
10
11
<div>我是父组件</div>
<div>
<span>问候语</span>
<input type="text" [(ngModel)]='hello'/>
</div>
<div>
<span>名字</span>
<input type="text" [(ngModel)]='user.name'>
</div>
<app-child [childHello]='hello' [childUser]='user'></app-child>
1
2
hello:string;
user:{name:string} = { name: '' };
子组件
1
2
3
<div>{{childHello}}</div>
<div>{{childUser.name}}</div>
<input type="text" [(ngModel)]="message"/>
1
2
3
4
5
6
7
8
@Input() childHello:string;
@input() childUser:{name:string};
message:string;
① ngOnChanges(changes:SimpleChanges):void{
console.log(changes);
}

以上代码结果表现为:

  • 当输入问候语hello时,①被触发。原因:由于字符串是不可变对象,然后输入后却发生了变化,故调用ngOnChanges;
  • 修改名字name时,①不触发。原因:由于修改的是可变对象(user的属性虽然变化了,但是user在内存中的地址依然没变),故不会触发ngOnChanges;
  • 当修改message时,①不处罚。原因:ngOnChanges钩子只针对输入属性,message为非输入属性;

虽然修改可变对象不会触发ngOnChanges钩子,但是子组件上的值已然发生了变化。这是由于Angular的变更检测机制仍然不活了组件中每个对象的属性的变化。


ngDoCheck

在这里,需要先重点说一下Angular的变更检测机制
  • 在Angular1.x中,任何原生事件都不会触发脏检查,必须使用NG事件才有效。如果使用了原生事件,你需要指定$apply()和$digest()来告诉Angular使用变更检测去处理它。
  • 在Angular2中,变更检测机制是由zone.js提供的,保证组件的属性的变化和页面的变化是同步的。

    • 浏览器中发生的任何事件都会触发变更检测,如:点击事件、输入数据;
    • 可以随意使用任何原生事件;

变更检测机制仅仅是将组件属性的改变反应到模块上,它并不会去改变组件属性的值。

变更检测
  • 每一个组件生成属于他们的变更检测器。当属性发生变化时,变更检测器指定的变更检测机制(default和Onush)就会响应,并判断是否需要更新模块;
  • 变更检测策略:
    • Default策略:采用Default策略的组件,无论组件树的哪个地方发生变化,都会被检测;检测顺序:根组件->子组件->孙组件;
    • Onpush策略:只有当组件的输入属性发生变化,变更检测机制才会检测该组件及其子组件;
对可变对象的检测:ngDoCheck
1
2
3
4
5
6
ngDoCheck():void{
if(this.user.name != this.oldname){
//do something
console.log(this.user.nane); //新值
}
}
  • 组件中只要有事件发生,ngDoCheck就会被调用①,比如:在两个文本框之间切换;
  • 由于①的原因,ngOnCheck非常容易被调用,使用时需要小心。对于ngOnCheck的实现要非常高效和轻量,否则容易因其性能问题;
  • 在Default策略下,每次变更检测,组件树中所有带check后缀的钩子都会被调用;

view钩子

  • 子组件

    1
    2
    3
    4
    5
    6
    getVal(val:string):void{
    console.log(val);
    }
    ①ngAfterViewInit():void{}
    ②ngAfterViewChecked():void{}
  • 父组件

    1
    2
    3
    4
    <div>我是父组件</div>
    <app-child #child1></app-child>
    <app-child #child2></app-child>
    <button (click)="child2.getVal('joy')"></button>
1
2
3
4
5
6
7
8
@ViewChild('child1') child1:ChildComponent;
ngOnInit():void{
this.child1.getVal('hahh');
}
③ngAfterViewInit():void{}
④ngAfterViewChecked():void{}

钩子调用顺序:①->②->③->④

  • 很明显,子组件必须在父组件之前组装完毕,init方法只会被调用一次;
  • 在变更检测周期中禁止修改属性,如果必须修改需要在子线程中操作;
    1
    2
    3
    4
    5
    6
    7
    8
    ngAfterViewInit():void{
    this.val = "hhahh"; //严重错误
    //解决方案
    setTimeout(()=>{
    this.val = "hahha";
    },0)
    }

ngContent指令

  • 一般写法

子组件

1
2
3
4
<div>
<div>我是子组件</div>
<ng-content></ng-content> //投影点
</div>

父组件

1
2
3
4
5
6
7
<div>
<div>我是父组件</div>
<app-child>
<div>这里面是投影内容</div>
<div>{{title}}</div> //虽然是投影在子组件中的内容,但是只能在父组件中操作
</app-child>
</div>

  • 多个投影的写法
    父组件
    1
    2
    3
    4
    5
    6
    7
    <div>
    <div>父组件</div>
    <app-child>
    <div class="firstPart">这是第一块内容</div>
    <div class="secondPart">这是第二块内容</div>
    </app-child>
    </div>

子组件

1
2
3
4
5
<div>
<div>子组件</div>
<ng-content select=".firstPart"></ng-content>
<ng-content select=".secondPart"></ng-content>
</div>


ngAfterContentInit、ngAfterContentChecked

  • 在投影内容组装完成之后调用、检测之后调用;

  • 执行顺序:ngAfterContentInit -> ngAfterContentChecked -> ngAfterContentInit -> ngAfterContentChecked -> ngAfterViewInit

  • 与ngAfterView的钩子不同,在ngAfterContent的钩子中可以改变属性的值


ngOnDestory

  • 当组件销毁的时候调用该钩子(配合路由使用);
  • 往往在该钩子中处理“反订阅一个流”、“清除定时器”等操作;

总结

  • 组件初始化

    ①constructor

    ②ngOnChanges

    ③ngOnInit

    ④ngDoCheck

    ⑤ngAfterContentInit

    ⑥ngAfterContentChecked

    ⑦ngAfterViewInit

    ⑧ngAfterViewChecked

    ⑨ngOnDestory

  • 组件销毁

    ⑨ngOnDestory

  • 变更检测

    ②ngOnChanges

    ④ngDoCheck

    ⑥ngAfterContentChecked

    ⑧ngAfterViewChecked

每一个组件都会经历三个阶段:初始化、变更检测、销毁

  • 当通过路由激活一个组件(按先后顺序):

    组件初始工作:

    • 启动constructor,提供所需实例化的对象

    • 如果有输入属性,启动ngOnChanges检测输入属性的变更

    • 启动ngOnInit,初始化一般数据

    • 启动ngDoCheck,进行一次变更检测

    开始组装组件:

    • 当组件中的投影内容被组装时,调用ngAfterContentInit

    • 启动ngAfterContentChecked,对投影内容检测

    • 当整个组件被组装时,调用ngAfterViewInit

    • 启动ngAfterViewChecked,对整个组件检测

      组件变更:

    • 当有事件发生(用户操作),启动ngDoCheck进行变更检测

    • 处理变更,如果需要更新组件,启动ngAfterContentChecked、ngAfterViewChecked;如果输入属性发生变化,启动ngOnChanges

    • 路由事件发生,启动ngOnDestory销毁组建,从头开始初始化一个组件;