Angular中所有的组件和指令都有相同的生命周期钩子,在@angular/core库中定义;可能很多初学者(包括我-。-)都只知道ngOnInit()和ngOnDestroy(),这里简单介绍一下Angular的几个生命周期的顺序和作用。
执行顺序
按照调用顺序,Angular的生命周期钩子有如下几个,
ngOnChanges() -> ngOnInit() -> ngDoCheck() -> ngAfterContentInit() -> ngAfterContentChecked() -> ngAfterViewInit() -> ngAfterViewChecked() -> ngOnDestroy()
作用
ngOnChanges()
这个钩子的第一次调用肯定会在ngOnInit()执行前触发,一般是用来检测组件/指令的输入属性发生的变化用的,一旦该组件的输入属性(@Input)发生变化,ngx就会出发这个组件的ngOnChanges()方法;
用法:
ngOnChanges(changes: SimpleChanges) { console.log(changes); // 第一次调用ngOnchanges()时,changes.prop.firstChange === true; // 且changes.prop.previousValue === undefined }复制代码
注意:
ngOnChanges()只能检测到输入属性的变化,检测不到内部属性的变化,也就是说这个钩子不像angular.js里的
$watch
方法,$watch()
方法是可以监视内部属性的变化并执行相应的回调函数的,而ngOnChanges()不可以;
即使是针对输入属性,如果输入属性是字符串,数字等不可变类型,ngOnChanges()会在输入属性发生变化时触发;
如果输入属性是数组,对象等引用类型,往数组里push等,直接修改object.key的值是不会触发ngOnChanges()的,Angular只关心属性的引用是否发生变化,而不会关注对象的某个属性是否发生变化。
如果你要在数组push时触发ngOnChanges(),则应该将push改写成
this.array = [...this.array, item]
。
修改对象key改写为
this.obj = Object.assign({}, this.obj, {name: 'blabla'})
或者this.obj = {...this.obj, name: 'blabla'}
这里主要使用了es7的展开符做了一个简单的示例,除此之外还可以通过immutable.js等不可变数据类型实现。
ngOnInit()
Angular在对一个组件第一次显示数据绑定和设置组件/指令的输入属性之后,初始化指令/组件。如果你用ng g component
创建组件的话,@angular/cli创建的组件文件中是会自带ngOnInit()的。
也就是说,ngOnInit()的执行是在组件/指令类的构造函数执行之后才会执行的,它只会执行一次。通常情况下,我们会把一些初始化逻辑放进ngOnInit()里面,如初始界面的数据的获取等。由于已经执行了构造函数,所以此时angular已经完成了输入属性的绑定。因此你可以放心的使用这些属性作为参数发送ajax请求。
ngOnDestroy()
Angular每次销毁组件/指令前会调用这个方法,通常在ngOnDestroy()里我们会对一些可观察对象进行取消订阅,对定时器进行取消等,防止内存泄漏;
有关取消订阅,事实上不是所有的可观察对象都需要你去手动取消订阅,详见
https://medium.com/@benlesh/rxjs-dont-unsubscribe-6753ed4fda87
ngDoCheck()
上面我们提到了ngOnChanges()里面无法对数组的push,对象的直接修改属性值等操作感知变化,当时我们采用了Object.assign和es7的展开符解决了这个问题。Angular本身提供了ngDoCheck()这个生命周期钩子来侦测这些他无法检测到的变更。
这个钩子类似angular.js中的$watch()方法,只要这个组件的属性发生变化,就会触发这个生命周期钩子。因此它的触发会很频繁,所以不要在里面写很复杂的逻辑,这样会导致程序的开销异常的大。
ngAfterContentInit()和ngAfterContentChecked()
这两个afterContent钩子都是在Angular进行内容投影时触发的。
ngAfterContentInit()第一次执行ngDoCheck()后执行,且只会执行一次。
内容投影就是将其他组件的html内容插入到本组件指定位置的方法。通常组件中会有一个
<ng-content></ng-content>
元素作为placeholder。
当组件的标签中有其他html标签时,说明存在内容投影。
ngAfterContentChecked()会在每次完成被投影组件的变更检测后执行,第一次执行是在第一次调用ngAfterContentInit()后执行。
这两个钩子发生在组件视图组合完成之前,所以此时仍然是可以更新组件属性而不用担心触发ExpressionChangedAfterItHasBeenCheckedError的。
ngAfterViewInit()和ngAfterViewChecked()
Angular会在每次创建了组件的子视图后调用他们(也就是组件中的子组件的视图);
要想访问子视图,需要通过@ViewChild()装饰的属性来实现。
// app.component.tsimport { Component, AfterViewInit, AfterViewChecked, ViewChild } from '@angular/core';import { DetailComponent } from './detail/detail.component';@Component({ selector: 'app-root', templateUrl: ``, styleUrls: []})export class AppComponent implements AfterViewInit, AfterViewChecked { @ViewChild(DetailComponent) detailView: DetailComponent; ngAfterViewInit() { console.log('after view init'); } ngAfterViewChecked() { console.log('after view checked, ', this.detailView); }}复制代码
// detail.component.tsimport { Component, OnInit } from '@angular/core';@Component({ selector: 'app-detail', templateUrl: ``, styleUrls: []})export class DetailComponent implements OnInit { public innerName = 'yyz'; constructor() { } ngOnInit() {}}复制代码
以上示例中,每次修改子组件中的input框的值,父组件中的ngAfterViewChecked()都会触发,并可以获取子组件中的属性值。ngAfterViewChecked()也是一个会被频繁调用的生命周期,所以里面的代码逻辑应当尽量精简;
另外,如果在ngAfterViewChecked()中直接修改组件属性,会触发一个错误ExpressionChangedAfterItHasBeenCheckedError: Expression has changed after it was checked。有关这个错误的具体原因可以另外在写一篇文章,所里这边不赘述了。因为Angular本身的单项数据流规则禁止在视图被组合好之后再更新视图。afterView的两个钩子都是在视图被组合好之后触发的。要避免这个错误,需要将操作推到浏览器下一个event loop。
ngAfterViewChecked() { console.log('after view checked, ', this.detailView); // this will throw one error // this.title = this.detailView.innerName + 'after'; if (this.detailName !== this.detailView.innerName + 'after') { setTimeout(() => { this.detailName = this.detailView.innerName + 'after'; }); }}复制代码
结束! 哈