Angular Lifecycle
今天我们来深入学习一下Angular的Lifecycle方法,Lifecycle方法是Angular中非常重要的一个概念,我们在开发中经常会用到这些方法,比如在ngOnInit
中初始化数据,或者在ngOnDestroy
中取消订阅等等。
首先在项目中生成一个组件,命名为lifecycle
,命令如下:
1 | ng g component lifecycle |
将lifecycle.component.html
中的内容清空.然后在lifecycle.component.ts
中添加如下代码。组件中的count
变量用来标记每个Lifecycle方法调用的序号,这样我们就可以清楚的看到每个方法的调用顺序了。
1 | import { |
运行程序,会得到如下输出:
1 | 1 constructor |
constructor
是构造函数,并不能算是Angular生命周期函数,但是为了图个全乎,我们一并介绍。
Constructor
constructor
是构造函数,它是在组件被创建时调用的,它的调用顺序是最早的,也就是说它是第一个被调用的方法,它的调用顺序是固定的,不会因为其他因素而改变。
构造函数中应该做哪些事情
一般在构造函数中会做一些初始化的工作,比如
- 初始化变量
- 订阅事件
构造函数中不应该做哪些事情?
- 与View相关的操作,比如操作DOM元素(应该在
ngAfterViewInit
中进行) - 获取后台数据(应该在
ngOnInit
中获取)
ngOnChanges
ngOnChanges
是当组件的@Input
属性发生变化时调用的,它接受一个SimpleChanges
类型的参数,这个参数中包含了变化的属性的信息,比如变化前的值和变化后的值等等。
调用时机:
- 当且仅当组件中有
@Input
属性时才会被调用。 - 先在
ngOnInit
之前调用一次。(为什么?) - 后续每当
@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
调用时机
- 在
ngOnChanges
之后调用一次。 - 不管
ngOnChanges
是否被调用,ngOnInit
都会被调用一次。 - 整个生命周期中只调用一次。
所以上面例子中构造函数调用之后,立即调用了ngOnInit
方法。
ngDoCheck
Detect and act upon changes that Angular can’t or won’t detect on its own
该方法主要用来做自定义的更新检测。
调用时机
2. 在ngOnInit
调用之后调用一次。
- 每次
ngOnChanges
调用之后,都会调用该方法。
在上例中,虽然没有调用ngOnChanges
,但是ngOnInit
调用了,所以该方法也调用了一次。
注意:这里的第一点Angular官网的解释并不准确,确切的说,是每次Angular进行更新检测之后,都会调用该方法,即使更新检测后,绑定的值没有任何变化,也会调用该方法。为了验证,我们可以在ngInit
中添加如下代码:
1 | constructor( |
此时观察控制台,输出如下,可见,每当change detection发生时,ngDoCheck
都会被调用。ngAfterContentChecked
和ngAfterViewChecked
也会跟着被调用。
1 | 1 constructor |
ngAfterContentInit
Respond after Angular projects external content into the component’s view。该方法与<ng-content>
标签相关。但是需要注意的是,无论组件中是否包含<ng-content>
标签,该方法都会被调用。
调用时机
- 在
ngDoCheck
第一次调用之后,调用一次。 - 整个生命周期中只调用一次。
ngAfterContentChecked
当Angular检测完组件内容变化之后调用。
调用时机
- 在
ngAfterContentInit
之后调用一次。 - 在每次
ngDoCheck
之后调用一次。
ngAfterViewInit
当Angular初始化完组件视图及其子视图之后调用。如果是directive中的ngAfterViewInit,则在初始化完包含该directive的视图之后调用。
调用时机
- 在
ngAfterContentChecked
第一次调用之后调用一次。 - 整个生命周期中只调用一次。
ngAfterViewChecked
当Angular检测完组件视图及其子视图之后调用。如果是directive中的ngAfterViewChecked,则在检测完包含该directive的视图之后调用。
调用时机
- 在
ngAfterViewInit
之后调用一次。 - 在每次
ngAfterContentChecked
之后调用一次。
ngOnDestroy
当Angular销毁组件之前调用。
调用时机
- 在组件被销毁之前调用。
- 整个生命周期中只调用一次。
要想看到该方法被调用,必须切换到切他页面,也就是离开该组件所在的页面才行。
下面我们改变页面内容,看看这些生命周期是否有变化,首先给模板文件添加内容,在lifecycle.component.html
中添加如下内容:
1 | <p>Lifecycle component works</p> |
保存并刷新页面,可以看到输出并未变化。
接下来我们给组件添加一个@Input
属性,修改lifecycle.component.ts
文件,添加如下内容:
1 | nameList: string[] = []; () |
修改模板文件,添加如下内容,用来显示输入的名字列表。
1 | <ng-container *ngFor="let name of nameList"> |
然后我们创建一个父组件,用来调用LifecycleComponent
组件,并传入nameList
属性。
lifecycle-parent.component.html
1 | <lifecycle-order [nameList]="nameList"> |
然后运行程序,切换到life-cycle页面,可以看到控制台输出如下内容,从第二行可以看出,ngOnChanges
方法被调用了,而且是在ngOnInit
之前调用的。
1 | 1 - OrderComponent: constructor |
由于我们并没有在父组件中修改nameList
属性,所以ngOnChanges
方法只被调用了一次。
我们可以打印一下changes
参数,看看里面有什么内容。
1 | ngOnChanges(simpleChanges: SimpleChanges) { |
控制台输出如下内容:
因为是第一次赋值,所以previousValue
是undefined
,currentValue
是['John, 'Mary', 'Joe']
。并且firstChange为true
。
接下来我们在父组件中添加一个按钮,用来修改nameList
属性,修改lifecycle-parent.component.html
文件,添加如下内容:
1 | <button (click)="changeNameList()">Change Name List</button> |
修改lifecycle-parent.component.ts
文件,添加如下内容:
1 | changeNameList() { |
运行程序,切换到life-cycle页面,点击按钮,可以看到控制台输出如下内容:可以看到,由于这次我们修改了nameList
属性,所以ngOnChanges
方法又被调了一次。
1 | 9 - OrderComponent: ngOnChanges |
这次changes
参数的内容如下图所示:
接下来,我们修改一下代码,添加一个input框,让用户输入名字,然后将该名字显示到页面上,修改lifecycle-parent.component.html
文件,添加如下内容:
1 | <input type="text" [(ngModel)]="name"> |
修改lifecycle-parent.component.ts
文件,添加如下内容:
1 | name: string; |
运行程序,输入zdd
到input框,点击Add Name按钮,可以看到新添加的name显示到了页面上,但是onChanges
方法并没有被调用,这是为什么呢?
这是因为,Angular默认的change detection比较的是Input值的引用
,而不是值本身。所以,当我们重新给nameList
赋值时,ngOnChanges
方法被调用了,因为此时nameList
的引用改变了,但是当我们使用Array.prototype.push
向nameList
中添加元素时,ngOnChanges
方法并没有被调用,因为nameList
的引用并没有变化。
要想让ngOnChanges
方法被调用,我们可以这样给nameList
属性赋值:
1 | this.nameList = [...this.nameList, this.name]; |
这样,nameList
的引用就变化了,ngOnChanges
方法就会被调用。
不知道大家是否注意到这样一个情况,我们在input框每输入一个字符,控制台都会打印一下内容,甚至在我们删除输入框内容的时候,也会打印。
1 | 96 - OrderComponent: ngDoCheck |
看来,每当我们输入值的时候,都触发了Angular change detection,这并不是我们想要的,我们只想在点击Add Name按钮的时候,触发change detection,这样才能保证性能。
用Angular Dev tool分析一下程序的性能。
首先打开Chrome的插件商店,搜索Angular DevTools
,然后安装该插件。
然后运行程序,打开该插件,切换到Profiler
页面。点击Start recording
,然后在input框中输入几个字符,并停止录制。
可以看到,输入框的input事件触发了OrderComponent
的change detection,这不是我们想要的。我们可以使用ChangeDetectorRef
来禁用change detection.
修改lifecycle.component.ts
文件,添加如下内容:
1 | constructor(private changeDetector: ChangeDetectorRef) {} |
再次运行程序,在input框中输入字符,观察控制台,你会发现,input事件不再触发change detection了。
1 | 1 |
使用ChangeDetectionStrategy.OnPush
可以提高性能,但是要注意,如果我们使用了ChangeDetectionStrategy.OnPush
,那么我们就必须使用@Input
属性,否则,ngOnChanges
方法不会被调用。而且使用这种策略时,只有当@Input
属性的引用
发生变化时,才会触发change detection,如果@Input
属性的值
发生变化,是不会触发change detection的。
比如,这样可以触发ngOnChanges
方法:
1 | addName() { |
但是这样不会触发ngOnChanges
方法:
1 | addName() { |
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 | ParentComponent.constructor |
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 | ParentComponent.constructor |
生命周期钩子 | 调用时机 | 调用次数 | 作用 |
---|---|---|---|
ngOnChanges |
输入属性(@Input )变化时触发 |
多次 | 响应输入属性的变化,获取新旧值并执行逻辑 |
ngOnInit |
组件初始化后触发(在首次 ngOnChanges 之后) |
一次 | 初始化数据(如从服务获取数据),适合执行一次性操作 |
ngDoCheck |
每次变更检测周期中触发 | 多次 | 手动检测变更(如复杂对象变更),用于扩展默认变更检测 |
ngAfterContentInit |
组件内容投影(如 <ng-content> )初始化完成后触发 |
一次 | 操作投影内容(如访问 @ContentChild 引用的子组件) |
ngAfterContentChecked |
每次内容投影变更检测完成后触发 | 多次 | 响应投影内容的变更(如动态插入子组件) |
ngAfterViewInit |
组件视图及子视图初始化完成后触发 | 一次 | 操作视图元素(如访问 @ViewChild 引用的 DOM 或子组件) |
ngAfterViewChecked |
每次视图及子视图变更检测完成后触发 | 多次 | 响应视图变更(如动态修改子组件属性),需避免在此修改状态以防止无限循环 |
ngOnDestroy |
组件销毁前触发 | 一次 | 清理资源(如取消订阅、移除事件监听器) |