0%

angular-lifecycle

Angular Lifecycle

今天我们来深入学习一下Angular的Lifecycle方法,Lifecycle方法是Angular中非常重要的一个概念,我们在开发中经常会用到这些方法,比如在ngOnInit中初始化数据,或者在ngOnDestroy中取消订阅等等。

首先在项目中生成一个组件,命名为lifecycle,命令如下:

1
ng g component lifecycle

lifecycle.component.html中的内容清空.然后在lifecycle.component.ts中添加如下代码。组件中的count变量用来标记每个Lifecycle方法调用的序号,这样我们就可以清楚的看到每个方法的调用顺序了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import {
AfterContentChecked,
AfterContentInit,
AfterViewChecked,
AfterViewInit,
Component,
DoCheck,
OnChanges,
OnDestroy,
OnInit,
} from '@angular/core';

@Component({
selector: 'app-order',
templateUrl: './order.component.html',
styleUrls: ['./order.component.less'],
})
export class OrderComponent
implements
OnInit,
AfterViewInit,
OnChanges,
AfterContentInit,
AfterContentChecked,
AfterViewChecked,
OnDestroy,
DoCheck
{
private static count = 0;

constructor() {
console.log(`${++OrderComponent.count} constructor`);
}

ngOnInit(): void {
console.log(`${++OrderComponent.count} ngOnInit`);
}

ngDoCheck() {
console.log(`${++OrderComponent.count} ngDoCheck`);
}

ngAfterContentInit() {
console.log(`${++OrderComponent.count} ngAfterContentInit`);
}

ngAfterContentChecked() {
console.log(`${++OrderComponent.count} ngAfterContentChecked`);
}

ngAfterViewInit() {
console.log(`${++OrderComponent.count} ngAfterViewInit`);
}

ngAfterViewChecked() {
console.log(`${++OrderComponent.count} ngAfterViewChecked`);
}

ngOnChanges() {
console.log(`${++OrderComponent.count} ngOnChanges`);
}

ngOnDestroy() {
console.log(`${++OrderComponent.count} ngOnDestroy`);
}
}

运行程序,会得到如下输出:

1
2
3
4
5
6
7
1 constructor
2 ngOnInit
3 ngDoCheck
4 ngAfterContentInit
5 ngAfterContentChecked
6 ngAfterViewInit
7 ngAfterViewChecked

constructor是构造函数,并不能算是Angular生命周期函数,但是为了图个全乎,我们一并介绍。

Constructor

constructor是构造函数,它是在组件被创建时调用的,它的调用顺序是最早的,也就是说它是第一个被调用的方法,它的调用顺序是固定的,不会因为其他因素而改变。

构造函数中应该做哪些事情

一般在构造函数中会做一些初始化的工作,比如

  1. 初始化变量
  2. 订阅事件

构造函数中不应该做哪些事情?

  1. 与View相关的操作,比如操作DOM元素(应该在ngAfterViewInit中进行)
  2. 获取后台数据(应该在ngOnInit中获取)

ngOnChanges

ngOnChanges是当组件的@Input属性发生变化时调用的,它接受一个SimpleChanges类型的参数,这个参数中包含了变化的属性的信息,比如变化前的值和变化后的值等等。

调用时机:

  1. 当且仅当组件中有@Input属性时才会被调用。
  2. 先在ngOnInit之前调用一次。(为什么?)
  3. 后续每当@Input属性发生变化时调用一次。

由于我们这个组件中没有@Input属性,所以这个方法没有被调用。

ngOnInit

Initialize the directive or component after Angular first displays the data-bound properties and sets the directive or component’s input properties

调用时机

  1. ngOnChanges之后调用一次。
  2. 不管ngOnChanges是否被调用,ngOnInit都会被调用一次。
  3. 整个生命周期中只调用一次。

所以上面例子中构造函数调用之后,立即调用了ngOnInit方法。

ngDoCheck

Detect and act upon changes that Angular can’t or won’t detect on its own

该方法主要用来做自定义的更新检测。

调用时机
2. 在ngOnInit调用之后调用一次。

  1. 每次ngOnChanges调用之后,都会调用该方法。

在上例中,虽然没有调用ngOnChanges,但是ngOnInit调用了,所以该方法也调用了一次。

注意:这里的第一点Angular官网的解释并不准确,确切的说,是每次Angular进行更新检测之后,都会调用该方法,即使更新检测后,绑定的值没有任何变化,也会调用该方法。为了验证,我们可以在ngInit中添加如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
constructor(
private changeDetector: ChangeDetectorRef, // 注入ChangeDetectorRef
) {
console.log(`${++OrderComponent.count} constructor`);
}

ngOnInit(): void {
console.log(`${++OrderComponent.count} ngOnInit`);

// 每隔一秒手动触发一次更新检测。
setInterval(() => {
this.changeDetector.detectChanges();
}, 1000);
}

此时观察控制台,输出如下,可见,每当change detection发生时,ngDoCheck都会被调用。ngAfterContentCheckedngAfterViewChecked也会跟着被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1 constructor
2 ngOnInit
3 ngDoCheck
4 ngAfterContentInit
5 ngAfterContentChecked
6 ngAfterViewInit
7 ngAfterViewChecked

8 ngDoCheck
9 ngAfterContentChecked
10 ngAfterViewChecked

11 ngDoCheck
12 ngAfterContentChecked
13 ngAfterViewChecked

...

ngAfterContentInit

Respond after Angular projects external content into the component’s view。该方法与<ng-content>标签相关。但是需要注意的是,无论组件中是否包含<ng-content>标签,该方法都会被调用。

调用时机

  1. ngDoCheck第一次调用之后,调用一次。
  2. 整个生命周期中只调用一次。

ngAfterContentChecked

当Angular检测完组件内容变化之后调用。

调用时机

  1. ngAfterContentInit之后调用一次。
  2. 在每次ngDoCheck之后调用一次。

ngAfterViewInit

当Angular初始化完组件视图及其子视图之后调用。如果是directive中的ngAfterViewInit,则在初始化完包含该directive的视图之后调用。

调用时机

  1. ngAfterContentChecked第一次调用之后调用一次。
  2. 整个生命周期中只调用一次。

ngAfterViewChecked

当Angular检测完组件视图及其子视图之后调用。如果是directive中的ngAfterViewChecked,则在检测完包含该directive的视图之后调用。

调用时机

  1. ngAfterViewInit之后调用一次。
  2. 在每次ngAfterContentChecked之后调用一次。

ngOnDestroy

当Angular销毁组件之前调用。

调用时机

  1. 在组件被销毁之前调用。
  2. 整个生命周期中只调用一次。

要想看到该方法被调用,必须切换到切他页面,也就是离开该组件所在的页面才行。

下面我们改变页面内容,看看这些生命周期是否有变化,首先给模板文件添加内容,在lifecycle.component.html中添加如下内容:

1
<p>Lifecycle component works</p>

保存并刷新页面,可以看到输出并未变化。

接下来我们给组件添加一个@Input属性,修改lifecycle.component.ts文件,添加如下内容:

1
@Input() nameList: string[] = [];

修改模板文件,添加如下内容,用来显示输入的名字列表。

1
2
3
<ng-container *ngFor="let name of nameList">
<p>name: {{name}}</p>
</ng-container>

然后我们创建一个父组件,用来调用LifecycleComponent组件,并传入nameList属性。
lifecycle-parent.component.html

1
2
<lifecycle-order [nameList]="nameList">
</lifecycle-order>

然后运行程序,切换到life-cycle页面,可以看到控制台输出如下内容,从第二行可以看出,ngOnChanges方法被调用了,而且是在ngOnInit之前调用的。

1
2
3
4
5
6
7
8
1 - OrderComponent: constructor
2 - OrderComponent: ngOnChanges
3 - OrderComponent: ngOnInit
4 - OrderComponent: ngDoCheck
5 - OrderComponent: ngAfterContentInit
6 - OrderComponent: ngAfterContentChecked
7 - OrderComponent: ngAfterViewInit
8 - OrderComponent: ngAfterViewChecked

由于我们并没有在父组件中修改nameList属性,所以ngOnChanges方法只被调用了一次。
我们可以打印一下changes参数,看看里面有什么内容。

1
2
3
4
ngOnChanges(simpleChanges: SimpleChanges) {
console.log(`${++OrderComponent.count} - ${this.className}: ngOnChanges`);
console.log(simpleChanges); // print changes
}

控制台输出如下内容:
Alt text
因为是第一次赋值,所以previousValueundefinedcurrentValue['John, 'Mary', 'Joe']。并且firstChange为true

接下来我们在父组件中添加一个按钮,用来修改nameList属性,修改lifecycle-parent.component.html文件,添加如下内容:

1
<button (click)="changeNameList()">Change Name List</button>

修改lifecycle-parent.component.ts文件,添加如下内容:

1
2
3
changeNameList() {
this.nameList = ['John', 'Mary', 'Joe', 'Tom'];
}

运行程序,切换到life-cycle页面,点击按钮,可以看到控制台输出如下内容:可以看到,由于这次我们修改了nameList属性,所以ngOnChanges方法又被调了一次。

1
2
3
4
9 - OrderComponent: ngOnChanges
10 - OrderComponent: ngDoCheck
11 - OrderComponent: ngAfterContentChecked
12 - OrderComponent: ngAfterViewChecked

这次changes参数的内容如下图所示:
Alt text

接下来,我们修改一下代码,添加一个input框,让用户输入名字,然后将该名字显示到页面上,修改lifecycle-parent.component.html文件,添加如下内容:

1
2
<input type="text" [(ngModel)]="name">
<button (click)="addName()">Add Name</button>

修改lifecycle-parent.component.ts文件,添加如下内容:

1
2
3
4
5
6
name: string;
addName() {
if (this.name.trim()) {
this.nameList.push(this.name);
}
}

运行程序,输入zdd到input框,点击Add Name按钮,可以看到新添加的name显示到了页面上,但是onChanges方法并没有被调用,这是为什么呢?

Alt text

这是因为,Angular默认的change detection比较的是Input值的引用,而不是值本身。所以,当我们重新给nameList赋值时,ngOnChanges方法被调用了,因为此时nameList的引用改变了,但是当我们使用Array.prototype.pushnameList中添加元素时,ngOnChanges方法并没有被调用,因为nameList的引用并没有变化。

要想让ngOnChanges方法被调用,我们可以这样给nameList属性赋值:

1
this.nameList = [...this.nameList, this.name];

这样,nameList的引用就变化了,ngOnChanges方法就会被调用。

不知道大家是否注意到这样一个情况,我们在input框每输入一个字符,控制台都会打印一下内容,甚至在我们删除输入框内容的时候,也会打印。

1
2
3
96 - OrderComponent: ngDoCheck
97 - OrderComponent: ngAfterContentChecked
98 - OrderComponent: ngAfterViewChecked

看来,每当我们输入值的时候,都触发了Angular change detection,这并不是我们想要的,我们只想在点击Add Name按钮的时候,触发change detection,这样才能保证性能。

用Angular Dev tool分析一下程序的性能。

首先打开Chrome的插件商店,搜索Angular DevTools,然后安装该插件。

然后运行程序,打开该插件,切换到Profiler页面。点击Start recording,然后在input框中输入几个字符,并停止录制。

Alt text

可以看到,输入框的input事件触发了OrderComponent的change detection,这不是我们想要的。我们可以使用ChangeDetectorRef来禁用change detection.

修改lifecycle.component.ts文件,添加如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor(private changeDetector: ChangeDetectorRef) {}

ngAfterViewInit() {
// 注意,不能在ngOnInit方法中调用以下方法,否则初始数据无法显示到页面上。
this.changeDetector.detach(); // 禁用change detection.
}

onInput(event: any) {
console.log(event.data);
}
addName() {
if (this.name.trim()) {
this.nameList = [...this.nameList, this.name];
this.changeDetector.detectChanges(); // 更新数据时,要手动触发change detection.
}
}

再次运行程序,在input框中输入字符,观察控制台,你会发现,input事件不再触发change detection了。

1
2
3
4
1
12
123
1234

使用ChangeDetectionStrategy.OnPush可以提高性能,但是要注意,如果我们使用了ChangeDetectionStrategy.OnPush,那么我们就必须使用@Input属性,否则,ngOnChanges方法不会被调用。而且使用这种策略时,只有当@Input属性的引用发生变化时,才会触发change detection,如果@Input属性的发生变化,是不会触发change detection的。

比如,这样可以触发ngOnChanges方法:

1
2
3
4
5
addName() {
if (this.name.trim()) {
this.nameList = [...this.nameList, this.name];
}
}

但是这样不会触发ngOnChanges方法:

1
2
3
4
5
addName() {
if (this.name.trim()) {
this.nameList.push(this.name);
}
}

Nested Components

In Angular, components can be nested, for example, a Parent component can contain a Child component. Here is the lifecycle method order for nested components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ParentComponent.constructor
ChildComponent.constructor
ParentComponent.ngOnChanges
ParentComponent.ngOnInit
ParentComponent.ngDoCheck
ParentComponent.ngAfterContentInit
ParentComponent.ngAfterContentChecked
ChildComponent.ngOnChanges
ChildComponent.ngOnInit
ChildComponent.ngDoCheck
ChildComponent.ngAfterContentInit
ChildComponent.ngAfterContentChecked
ChildComponent.ngAfterViewInit
ChildComponent.ngAfterViewChecked
ParentComponent.ngAfterViewInit
ParentComponent.ngAfterViewChecked
ChildComponent.ngOnDestroy
ParentComponent.ngOnDestroy

Deep nested components

What if the Child component also has a child component Descendant? Here is the lifecycle method order for deep nested components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
ParentComponent.constructor
ChildComponent.constructor
DescendantComponent.constructor
ParentComponent.ngOnChanges
ParentComponent.ngOnInit
ParentComponent.ngDoCheck
ParentComponent.ngAfterContentInit
ParentComponent.ngAfterContentChecked
ChildComponent.ngOnChanges
ChildComponent.ngOnInit
ChildComponent.ngDoCheck
ChildComponent.ngAfterContentInit
ChildComponent.ngAfterContentChecked
DescendantComponent.ngOnChanges
DescendantComponent.ngOnInit
DescendantComponent.ngDoCheck
DescendantComponent.ngAfterContentInit
DescendantComponent.ngAfterContentChecked
DescendantComponent.ngAfterViewInit
DescendantComponent.ngAfterViewChecked
ChildComponent.ngAfterViewInit
ChildComponent.ngAfterViewChecked
ParentComponent.ngAfterViewInit
ParentComponent.ngAfterViewChecked
DescendantComponent.ngOnDestroy
ChildComponent.ngOnDestroy
ParentComponent.ngOnDestroy
生命周期钩子 调用时机 调用次数 作用
ngOnChanges 输入属性(@Input)变化时触发 多次 响应输入属性的变化,获取新旧值并执行逻辑
ngOnInit 组件初始化后触发(在首次 ngOnChanges 之后) 一次 初始化数据(如从服务获取数据),适合执行一次性操作
ngDoCheck 每次变更检测周期中触发 多次 手动检测变更(如复杂对象变更),用于扩展默认变更检测
ngAfterContentInit 组件内容投影(如 <ng-content>)初始化完成后触发 一次 操作投影内容(如访问 @ContentChild 引用的子组件)
ngAfterContentChecked 每次内容投影变更检测完成后触发 多次 响应投影内容的变更(如动态插入子组件)
ngAfterViewInit 组件视图及子视图初始化完成后触发 一次 操作视图元素(如访问 @ViewChild 引用的 DOM 或子组件)
ngAfterViewChecked 每次视图及子视图变更检测完成后触发 多次 响应视图变更(如动态修改子组件属性),需避免在此修改状态以防止无限循环
ngOnDestroy 组件销毁前触发 一次 清理资源(如取消订阅、移除事件监听器)

Reference

  1. https://angular.dev/guide/components/lifecycle#summary