Angular 生命周期实际
生命周期 hooks 的意义
在 Angular 中,实现组件的生命周期 hook 接口办法是应用 Angular 框架实现插入业务的机会点。
官网文档中生命周期都有哪些内容
在 Angular 的官网文档中有一个章节 Lifecycle hooks 来专门解说生命周期的机制与如何来察看组件生命周期。
从官网章节中,咱们能理解到生命周期 hook 办法 的方方面面并且根本不会有出入。
我列举几点从官网播种到的:
- 在组件中的写法;要实现
OnInit
办法,在组件中增加ngOnInit
办法, 留神多了俩个字母,每个 hook 办法均是如此。 - 生命周期事件程序;
- 如何去察看生命周期事件;
- 应用 ngOnInit 作为业务插入点,而不应该在 constructor 中放入业务代码;
- ngOnDestroy 的执行机会
- 全副生命周期事件触发程序
- ngOnChanges 的执行机制
- … 等等
这基本上笼罩到了有组件生命周期的方方面面;
本文重点内容
官网章节内容是基于生命周期自身去讲的,而理论应用过程生命周期的执行过程会与 依赖注入机会 , 数据流传递 , 变化检测 , 父子组件数据变更 , 组件继承 等等诸多个性联合应用,在联合后,执行程序是怎么的对咱们来说有点不那么容易弄明确,只有通过大量思考以及实际后能力得悉并总结出其法则。
与官网的切入点不同,本文心愿从实际中来总结法则。
而本文选取了如下几种场景,进行实际演示,并尝试得出法则:
- 根本的生命周期介绍
- 数据流传递的机会是在哪个具体的事件执行。
- 父子组件中父子组件的生命周期是如何执行的。
- 继承组件中生命周期又是如何执行的。
- 模板中绑定的变量在获取值时与这些生命周期有没有关系?
心愿通过本文的浏览,能帮你拨开一些面纱,让你可能随我一起进一步把握 Angular 的个性,去思考 Angular 中生命周期设定去窥探 Angular 的运行机制;了解 Angular 的思路、思维以及其为开发者塑造的思考模式。
只有咱们可能依照 Angular 的形式思考,当遇到简单的业务参加了简单的数据流,简单的组件关系,简单的类关系后,能通过思考疾速理解常识盲区,疾速组织获取相干常识的关键词,这样茫茫 Angular 的概念以及新常识的陆地里你就变得蛟龙得水,也会成为咱们开发业务代码时,优化重构,解决问题时最尖锐的矛。
lifecycle-hook-basic
因为官网文档中曾经把生命周期的程序介绍并演示了,咱们这里就做个简略的验证和总结,当做一次温习。
首先来看一个理论执行效果图
留神:有认真的小伙伴可能会留神到这个图中,没有看到 ngOnChanges 事件。不是忘了 根组件不会触发 ngOnChanges 事件,因为跟组件没有 @Input 变量;
源码如下
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {this.subject.subscribe((item) => {this.messages.push(item);
});
this.subject.next({type: 'constructor exec', content: 'AppComponent class instance'});
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnChanges'});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnInit'});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngDoCheck'});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentInit'});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentChecked'});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewInit'});
}
ngAfterViewChecked() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewChecked'});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnDestroy'});
}
}
下图是失常组件,并带有 @Input 属性
源码如下
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {this.subject.subscribe((item) => {this.messages.push(item);
});
this.subject.next({type: 'constructor exec', content: 'AppComponent class instance'});
}
}
// HelloComponent 是 AppComponent 的子视图组件
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato;}`],
})
export class HelloComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {this.subject.next({ type: '@input', content: 'set name update'});
this._name = n;
}
get name() {// this.subject.next({ type: 'template binding variable get', content: 'get name update'}); 仅演示调用
return this._name;
}
messages = [];
constructor() {this.subject.next({ type: 'constructor exec', content: 'class instance, 拜访 @input 属性 name=' + this.name});
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 拜访 @input 属性 name=' + this.name});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 拜访 @input 属性 name=' + this.name});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewChecked() {this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 拜访 @input 属性 name=' + this.name});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 拜访 @input 属性 name=' + this.name});
}
}
解释并演绎
咱们联合下面程序能够先粗略画一个图如下:
解释下这个图中的每个钩子的含意:
留神:
- 浅灰色名字的事件,在组件的生命周期中只会触发一次,而绿色的随着相应的逻辑变动会屡次触发。
- 这里我将组件构造函数的执行也退出到了察看序列中,因为在业务中,常常会有小伙伴会在 constructor 中插入业务代码。
所有的办法执行程序如下:
Construction, OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy
Construction
构造函数执行时,执行一次。
OnChanges
当指令(组件的实现继承了指令)的任何一个可绑定属性发生变化时调用。
要点:
- 从下面根组件的执行成果来看,这个 hook 不肯定会被调用,只有 @Input 属性变动才会触发;
- @Input 属性的变更次数是没有要求的,所以它的回调次数也是没有限度的。
- 这块要特地留神父级扭转传递值的状况,会导致 ngOnChange 在生命周期的任何时刻都可能被再次调用。
- 参数是一个输出属性的变更处理器,会蕴含所有变更的输出属性的旧值和新值;
- 如果至多产生了一次变更,则该回调办法会在默认的变更检测器查看完可绑定属性之后、视图子节点和内容子节点查看完之前调用。
- 属性的 setter 可能代替在这个钩子中执行逻辑。
OnInit
在 Angular 初始化完了该指令的所有数据绑定属性之后调用;
要点:
- 在默认的变更检测器首次查看完该指令的所有数据绑定属性之后,任何子视图或投影内容查看完之前。
- 它会且只会在指令初始化时调用 一次
- 定义
ngOnInit()
办法能够解决所有 附加的初始化工作。
DoCheck
在变更检测期间,默认的变更检测算法会依据援用来比拟可绑定属性,以查找差别。你能够应用此钩子来用其余形式检查和响应变更。
要点:
- 除了应用默认的变更查看器执行查看之外,还会为指令执行自定义的变更检测函数
- 默认变更检测器查看更改时,他会触发 OnChanges()的执行(如果有),而不在乎你是否进行了额定的变更检测。
- 不应该同时应用 DoCheck 和 OnChanges 来响应在同一个输出上产生的更改。
- 默认的变更检测器执行之后调用,并进行变更检测。
- 参见
KeyValueDiffers
和IterableDiffers
,以实现针对汇合对象的自定义变更检测逻辑。 - 在 DoCheck 中你能够实现监控那些 OnChanges 无奈捕捉的变更,检测的逻辑须要自行实现。
- 因为 DoCheck 能够监控出特定变量的何时产生了变动,但这却十分低廉。Angular 在页面的其它中央 渲染 不相干的数据 也会触发这个钩子,所以你的实现必须自行保障用户体验。
AfterContentInit
它会在 Angular 初始化完该指令的 所有内容 之后立刻调用。
要点:
- 在指令初始化实现之后,它只会调用一次。
- 能够用来解决一些 初始化工作
AfterContentChecked
在默认的变更检测器对该指令下的所有内容实现了变更检测之后立刻调用。
AfterViewInit
- 在 Angular 齐全初始化了 组件的视图 后调用。定义一个
ngAfterViewInit()
办法来解决一些 额定的初始化工作。
AfterViewChecked
在默认的变更检测器对组件视图实现了一轮 变更检测周期 之后立刻调用。
OnDestroy
在指令、管道或服务被销毁时调用。用于在实例被销毁时,执行一些自定义清理代码。
进一步阐明:
因为咱们关怀的是什么时候应用这些钩子,大家回到下面这些回调钩子定义处,仔细观察带有 Init 的钩子内容,能够看到 OnInit,AfterContentInit,AfterViewInit 这三个,他们都只执行一次,都能够做一些初始化工作,这三个钩子的区别,就像定义中的形容都是有差异的。
大家有须要能够在文章上面留言,看理论状况是否须要认真解释下,这三个钩子所实用的差异化场景。
没有认真钻研过这三者不同的小伙伴,经常对于应该把本人要实现的异步初始化业务放到哪个钩子中昏头昏脑,所以轻易选一个,要么放在 OnInit, 要么 AfterViewInit,如果发现放到一个里不行,就换另一个,直到问题解决或消耗很长时间问题解决不了或者留下偶现的 bug(之所以偶现是因为没有从技术上保障异步的程序性执行),再次排查也相当吃力。
所以这三个 Init 十分值得写异步业务比拟多的小伙伴关注。
从第一个场景下,咱们回顾了每一个生命周期钩子都有哪些内容。
接下来咱们看一下带有 @Input 的场景:
lifecycle-hook&Input
源码如下
//AppComponent html
<h1>Hi, Angular 13!</h1>
<h3>- 演示生命周期钩子函数调用程序 <br /></h3>
<p>Start editing to see some magic happen :)</p>
<ul>
<li *ngFor="let message of messages">
<span class="message-type">{{message.type}}</span>
=>
<span class="message-content">{{message.content}}</span>
</li>
</ul>
<hello [name]="name"></hello>
// AppComponent
@Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent{
name = 'Angular' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {this.subject.subscribe((item) => {this.messages.push(item); });
this.subject.next({type: 'constructor exec', content: 'AppComponent class instance, 拜访 @input 属性 name=' + this.name});
}
}
// HelloComponent 是 AppComponent 的子视图组件
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato;}`],
})
export class HelloComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {this.subject.next({ type: '@input', content: 'set name update'});
this._name = n;
}
get name() {// this.subject.next({ type: 'template binding variable get', content: 'get name update'}); 仅演示调用
return this._name;
}
messages = [];
constructor() {this.subject.next({ type: 'constructor exec', content: 'class instance, 拜访 @input 属性 name=' + this.name});
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 拜访 @input 属性 name=' + this.name});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 拜访 @input 属性 name=' + this.name});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewChecked() {this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 拜访 @input 属性 name=' + this.name});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 拜访 @input 属性 name=' + this.name});
}
}
执行后果
解释并演绎
咱们给 Hello 组件增加了输出属性,父组件 AppComponent 初始化时给 name 赋值了初始值,仅在 angular 的解决下输出属性的绑定是产生在 Hello 组件初始化之前(当然在 Hello 组件生命周期调用的过程中,父组件随时可能扭转 name)。
- 留神在第一次 OnChanges 触发之后,也就是传递变量的初始值给完后,有些状况咱们会通过逻辑在父组件中调整传递变量的值,这时就会立刻再次触发 OnChanges 的回调,并且这个回调与 HelloComponent 组件的 OnInit,AfterContentInit 等的回调是按工夫程序顺次调用的。也就是 OnChanges 的触发与 AfterContentInit,AfterViewInit 是否曾经实现一次执行无关。
咱们在刚刚的 AppComponent 组件中也退出生命周期的执行,后果会怎么样呢?
lifecycle-hook&child&parent&Input
改变源码如下
export class AppComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {
name = 'Angular' + VERSION.major;
messages = [];
subject = window['subject'];
constructor() {this.subject.subscribe((item) => {this.messages.push(item); });
this.subject.next({type: 'constructor exec', content: 'AppComponent class instance, 拜访 @input 属性 name=' + this.name});
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnChanges'});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnInit'});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngDoCheck'});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentInit'});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterContentChecked'});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewInit'});
}
ngAfterViewChecked() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngAfterViewChecked'});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'AppComponent ngOnDestroy'});
}
}
执行后果
解释并演绎
景象:
- 父组件 AppComponent 的生命周期回调被子组件分为了俩局部。
- constuctor 的执行程序是先父组件,接下来是子组件,接下来才是生命周期的其余函数回调。
- 父组件的 ngOnChanges(如果有,第一次), ngOnInit,ngDoCheck,ngAfterContentInit,ngAfterContentChecked 会执行较早。
- 父组件 name 值传递到子组件,触发子组件的 OnChanges。
- 子组件的生命周期执行,接下来父组件的 ngAfterViewInit,ngAfterViewChecked 执行。
要点:
-
父组件将绑定值传入到子组件是在父组件的生命周期执行到 ngAfterContentChecked 时触发,这一点很重要
- 意味着,如果在子组件的生命周期(比方:OnInit)中有解决依赖传递变量的逻辑,那么可能得不到最新的传递值。(因为这一点,小伙伴常常陷入困惑中,这也与不理解 Init 钩子的实用场景无关)
- 父子组件中,AfterViewInit 会等到所有的子组件的生命周期执行实现才执行,(这一点个性应该被充分发挥并利用)。
接下来咱们看看存在继承组件的场景下,Angular 会怎么解决生命周期回调。
life-hook&child&parent&inheritComponent&input
改变源码如下:
...
// 这一次咱们增加了一个 BaseComponent 作为 Hello 组件的基类,在 Angular 中是以 Directive 来装璜的
// 应用 Directive 的益处
// Angular 组件继承不会继承元数据,能够应用 directive 装璜器元数据可配置空来防止配置多余的元数据
// Directive 是 Component 装璜器的基类,根本无缝替换
@Directive()
export class BaseComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked,
AfterViewInit, AfterViewChecked, OnDestroy {subject = window['subject'];
constructor() {this.subject.next({ type: 'constructor exec', content: 'BaseComponent class instance'});
}
_name: string = '';
@Input() set name(n: string) {this.subject.next({ type: '@input', content: 'set base name update'});
this._name = n;
}
get name() {
// 非必要不定义 getter 或者不放逻辑,拜访次数十分多
// this.subject.next({type: 'tpl binding variable get', content: 'get name update'});
return this._name;
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnChanges'});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnInit'});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngDoCheck'});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterContentInit'});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterContentChecked'});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterViewInit'});
}
ngAfterViewChecked() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngAfterViewChecked'});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'BaseComponent ngOnDestroy'});
}
}
// HelloComponent 是 AppComponent 的子视图组件,同时也是 BaseComponent 的子类
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato;}`],
})
export class HelloComponent extends BaseComponent {
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {this.subject.next({ type: '@input', content: 'set name update'});
this._name = n;
}
get name() {// this.subject.next({ type: 'template binding variable get', content: 'get name update'}); 仅演示调用
return this._name;
}
messages = [];
constructor() {super();
this.subject.next({type: 'constructor exec', content: 'class instance, 拜访 @input 属性 name=' + this.name});
}
}
执行后果
解释演绎
-
景象及实现状况
- 生命周期去掉了继承体系的 HelloComponent 中的实现,在基类 BaseComponent 中实现
- 无论如何结构器都是在最早执行并案依赖程序执行
- BaseComponent 中的生命周期都执行了(咱们晓得继承后,这些生命周期办法在 Hello 组件中也是能够调用的,那 Angular 到底调用的是子类的还是基类的呢?请持续往下看)
持续批改源码:
// HelloComponent 是 AppComponent 的子视图组件,同时也是 BaseComponent 的子类
...
@Component({
selector: 'hello',
template: `<h1>Hi, {{name}}!</h1>`,
styles: [`h1 { font-family: Lato;}`],
})
export class HelloComponent extends BaseComponent implements OnChanges, OnInit, DoCheck, AfterContentInit, AfterContentChecked, AfterViewInit, AfterViewChecked, OnDestroy{
_name: string = '';
subject = window['subject'];
@Input() set name(n: string) {this.subject.next({ type: '@input', content: 'set name update'});
this._name = n;
}
get name() {// this.subject.next({ type: 'template binding variable get', content: 'get name update'}); 仅演示调用
return this._name;
}
messages = [];
constructor() {super();
this.subject.next({type: 'constructor exec', content: 'class instance, 拜访 @input 属性 name=' + this.name});
}
ngOnChanges() {this.subject.next({ type: 'lifecycle', content: 'ngOnChanges, 拜访 @input 属性 name=' + this.name});
}
ngOnInit() {this.subject.next({ type: 'lifecycle', content: 'ngOnInit, 拜访 @input 属性 name=' + this.name});
}
ngDoCheck() {this.subject.next({ type: 'lifecycle', content: 'ngDoCheck, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterContentChecked() {this.subject.next({ type: 'lifecycle', content: 'ngAfterContentChecked, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewInit() {this.subject.next({ type: 'lifecycle', content: 'ngAfterViewInit, 拜访 @input 属性 name=' + this.name});
}
ngAfterViewChecked() {this.subject.next({ ype: 'lifecycle', content: 'ngAfterViewChecked, 拜访 @input 属性 name=' + this.name});
}
ngOnDestroy() {this.subject.next({ type: 'lifecycle', content: 'ngOnDestroy, 拜访 @input 属性 name=' + this.name});
}
}
咱们为子类也实现这些生命周期,看看 Angualr 执行的是子类的还是父类的还是俩个都会执行,执行的程序如何?
执行后果
演绎总结:
-
景象是执行了子类的,父类的没有执行
- 阐明生命周期办法,子类和父类之间生命周期办法也合乎继承原理存在重写的状况,子类中的生命周期办法
模板绑定变量获取的状况
最初顺道再看一下模板绑定的变量 Angular 获取的状况,如下图
get name() { // 只须要把 name 的 getter 中的正文去掉即可
this.subject.next({type: 'template binding variable get', content: 'get name update'}); 仅演示调用
return this._name;
}
// 另一个,须要把生命周期钩子中打印 name 字段读取去掉,这样咱们就晓得 name 被 Angular 读取了几次,并在什么时候读取。(有时候,咱们会在 getter 中写一些简略的逻辑,把变量作为计算属性,理解这个对咱们通晓 name 被读取的数量,有很大用途)
解释并演绎
- 父组件将 name 属性传递给 Hello 组件(@input 执行)后,Hello 组件将本人的内容 ngAfterContentChecked 后获取 name, 并在 App 组件 ngAfterViewChecked 后读取了 name 的值,整个过程读取了俩次。
最初再附上一张对于联合上述三个组件的 Angular 我的项目的运行以及生命周期执行运行图
最初,如果大家对上述表述或者论断存在质疑或者不解,能够在上面留言。