在介绍 Angular 2 Directive Lifecycle (生命周期) 之前,我们先来介绍一下 Angular 2 中 Directive (指令) 与 Component (组件) 的关系。
我们再来看一下 Angular 2 中定义的指令和组件接口:
// angular2/packages/core/src/Metadata/directives.ts export interface Directive { selector?: string; // 用于定义组件在HTML代码中匹配的标签 inputs?: string[]; // 指令的输入属性 outputs?: string[]; // 指令的输出属性 host?: {[key: string]: string}; // 绑定宿主的属性、事件等 providers?: Provider[]; // 设置指令及其子指令可以用的服务 exportAs?: string; // 导出指令,使得可以在模板中调用 queries?: {[key: string]: any}; // 设置指令的查询条件 } export interface Component extends Directive { changeDetection?: ChangeDetectionStrategy; // 指定组件使用的变化检测策略 viewProviders?: Provider[]; // 设置组件及其子组件(不含ContentChildren)可以用的服务 moduleId?: string; // 包含该组件模块的 id,它被用于解析 模版和样式的相对路径 templateUrl?: string; // 为组件指定一个外部模板的URL地址 template?: string; // 为组件指定一个内联的模板 styleUrls?: string[]; // 为组件指定一系列用于该组件的样式表文件 styles?: string[]; // 为组件指定内联样式 animations?: any[]; // 设置组件相关动画 encapsulation?: ViewEncapsulation; // 设置组件视图包装选项 interpolation?: [string,string]; // 设置默认的插值运算符,默认是"{{"和"}}" entryComponents?: Array<Type<any>|any[]>; // 设置需要被提前编译的组件 }
通过观察上图与 Angular 2 中指令与组件的接口定义,我们可以总结出指令与组件之间的关系:组件继承于指令,并扩展了与 UI 视图相关的属性,如 template、styles、animations、encapsulation 等。
下面我们进入正题,开始介绍 Angular 2 指令的生命周期,它是用来记录指令从创建、应用及销毁的过程。Angular 2 提供了一系列与指令生命周期相关的钩子,便于我们监控指令生命周期的变化,并执行相关的操作。Angular 2 中所有的钩子如下图所示:
怎么那么多钩子,是不是被吓到了,没事我们基于指令与组件的区别来分个类:
-
指令与组件共有的钩子
ngOnChanges
ngOnInit
ngDoCheck
ngOnDestroy
-
组件特有的钩子
ngAfterContentInit
ngAfterContentChecked
ngAfterViewInit
ngAfterViewChecked
Angular 2 指令生命周期钩子的作用及调用顺序
ngOnInit - 在第一次 ngOnChanges 后调用
ngAfterViewInit - 组件相应的视图初始化之后调用
ngAfterViewChecked - 组件每次检查视图时调用
ngOnDestroy - 指令销毁前调用
Angular 2 指令生命周期钩子详解
在详细介绍指令生命周期钩子之前,我们先来介绍一下构造函数:
constructor
组件的构造函数会在所有的生命周期钩子之前被调用,它主要用于依赖注入或执行简单的数据初始化操作。
import { Component,ElementRef } from '@angular/core'; @Component({ selector: 'my-app',template: ` <h1>Welcome to Angular World</h1> <p>Hello {{name}}</p> `,}) export class AppComponent { name: string = ''; constructor(public elementRef: ElementRef) { // 使用构造注入的方式注入依赖对象 this.name = 'Semlinker'; // 执行初始化操作 } }
ngOnChanges
当数据绑定输入属性的值发生变化的时候,Angular 将会主动调用 ngOnChanges 方法。它会获得一个 SimpleChanges 对象,包含绑定属性的新值和旧值,它主要用于监测组件输入属性的变化。
app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'my-app',template: ` <h4>Welcome to Angular World</h4> <exe-child name="exe-child-component"></exe-child> `,}) export class AppComponent { }
child.component.ts
import { Component,Input,SimpleChanges,OnChanges } from '@angular/core'; @Component({ selector: 'exe-child',template: ` <p>Child Component</p> <p>{{ name }}</p> ` }) export class ChildComponent implements OnChanges{ @Input() name: string; ngOnChanges(changes: SimpleChanges) { console.dir(changes); } }
ngOnInit
在第一次 ngOnChanges 执行之后调用,并且只被调用一次。它主要用于执行组件的其它初始化操作或获取组件输入的属性值。
import { Component,OnInit } from '@angular/core'; @Component({ selector: 'exe-child',template: ` <p>父组件的名称:{{pname}} </p> ` }) export class ChildComponent implements OnInit { @Input() pname: string; // 父组件的名称 constructor() { console.log('ChildComponent constructor',this.pname); // Output:undefined } ngOnInit() { console.log('ChildComponent ngOnInit',this.pname); // output: 输入的pname值 } }
ngOnDestory
在指令被销毁前,将会调用 ngOnDestory 方法。它主要用于执行一些清理操作,比如:移除事件监听、清除定时器、退订 Observable 等。
@Directive({ selector: '[destroyDirective]' }) export class OnDestroyDirective implements OnDestroy { sayHello: number; constructor() { this.sayHiya = window.setInterval(() => console.log('hello'),1000); } ngOnDestroy() { window.clearInterval(this.sayHiya); } }
ngDoCheck
当组件的输入属性发生变化时,将会触发 ngDoCheck 方法。我们可以使用该方法,自定义我们的检测逻辑。它也可以用来加速我们变化检测的速度。
ngAfterContentInit
在组件使用 ng-content
指令的情况下,Angular 会在将外部内容放到视图后用。它主要用于获取通过 @ContentChild 或 @ContentChildren 属性装饰器查询的内容视图元素。
具体使用示例,请参考 - Angular 2 ContentChild & ContentChildren
ngAfterContentChecked
在组件使用 ng-content
指令的情况下,Angular 会在检测到外部内容的绑定或者每次变化的时候调用。
ngAfterViewInit
在组件相应的视图初始化之后调用,它主要用于获取通过 @ViewChild 或 @ViewChildren 属性装饰器查询的视图元素。
具体使用示例,请参考 - Angular 2 ViewChild & ViewChildren
ngAfterViewChecked
组件每次检查视图时调用
Angular 2 LifecycleHooks 、SimpleChanges 等相关接口
LifecycleHooks 接口
export interface OnChanges { ngOnChanges(changes: SimpleChanges): void; } export interface OnInit { ngOnInit(): void; } export interface DoCheck { ngDoCheck(): void; } export interface OnDestroy { ngOnDestroy(): void; } export interface AfterContentInit { ngAfterContentInit(): void; } export interface AfterContentChecked { ngAfterContentChecked(): void; } export interface AfterViewInit { ngAfterViewInit(): void; } export interface AfterViewChecked { ngAfterViewChecked(): void; }
SimpleChange
// 用于表示变化对象 export class SimpleChange { constructor(public prevIoUsValue: any,public currentValue: any,public firstChange: boolean) {} // 标识是否为首次变化 isFirstChange(): boolean { return this.firstChange; } }
SimpleChanges
export interface SimpleChanges { [propName: string]: SimpleChange; }
Angular 2 View 详解
在 Angular 2 中 View (视图) 由三个部分组成:
Elements - 元素
Bindings - 绑定
Events - 事件
在 Angular 2 TemplateRef & ViewContainerRef 这篇文章中,我们介绍了 Angular 2 支持的 View(视图) 类型:
Embedded Views - Template 模板元素
Host Views - Component 组件
接下来我们来分析一下组件对应的 Host Views,具体示例如下:
child.component.ts
import { Component,OnChanges,AfterViewChecked } from '@angular/core'; @Component({ selector: 'exe-child',template: ` <p>Child Component</p> <p>{{ name }}</p> ` }) export class ChildComponent implements OnChanges,AfterViewChecked{ @Input() name: string; ngOnChanges(changes: SimpleChanges) { console.dir(changes); setTimeout(() => { this.name = 'exe-child-component-1' },0); } ngAfterViewChecked() { console.log('ngAfterViewChecked hook has been called'); } }
app.component.ts
import { Component } from '@angular/core'; @Component({ selector: 'my-app',}) export class AppComponent { }
接下来我们来分析一下 ChildComponent 组件,先来看一下编译后的 component.ngfactory.js 文件。
ChildComponent/component.ngfactory.js 代码片段:
function View_ChildComponent0(viewUtils,parentView,parentIndex,parentElement) { var self = this; ... self._expr_7 = jit_CD_INIT_VALUE5; } /* * 用于初始化模板内的元素 * ChildComponent - template * <p>Child Component</p> * <p>{{ name }}</p> */ View_ChildComponent0.prototype.createInternal = function(rootSelector) { var self = this; var parentRenderNode = self.renderer.createViewRoot(self.parentElement); ... // (1) 创建p元素 - <p>Child Component</p> self._el_1 = jit_createRenderElement6(self.renderer,parentRenderNode,'p',jit__object_Object_7,self.debug(1,1,6)); // 创建文本元素,设置内容为 - 'Child Component' self._text_2 = self.renderer.createText(self._el_1,'Child Component',self.debug(2,9)); // (2) 创建p元素 - <p>{{ name }}</p> self._el_4 = jit_createRenderElement6(self.renderer,self.debug(4,2,6)); self._text_5 = self.renderer.createText(self._el_4,'',self.debug(5,9)); self.init(null,(self.renderer.directRenderer? null: [...] ),null); return null; }; // 执行变化检测 View_ChildComponent0.prototype.detectChangesInternal = function(throwOnChange) { var self = this; self.debug(5,9); var currVal_7 = jit_inlineInterpolate8(1,self.context.name,''); if (jit_checkBinding9(throwOnChange,self._expr_7,currVal_7)) { self.renderer.setText(self._text_5,currVal_7); self._expr_7 = currVal_7; } };
ChildComponent/wrapper.ngfactory.js 代码片段:
function Wrapper_ChildComponent() { var self = this; self._changed = false; self._changes = {}; // 创建Changes对象 self.context = new jit_ChildComponent0(); self._expr_0 = jit_CD_INIT_VALUE1; // {} } Wrapper_ChildComponent.prototype.ngOnDestroy = function() { }; Wrapper_ChildComponent.prototype.check_name =function(currValue,throwOnChange,forceUpdate) { var self = this; // 判断值是否更新,jit_checkBinding2中直接使用looseIdentical(oldValue,newValue) // 进行全等比较(===) if ((forceUpdate || jit_checkBinding2(throwOnChange,self._expr_0,currValue))) { self._changed = true; self.context.name = currValue; // 创建name关联的SimpleChange对象 self._changes['name'] = new jit_SimpleChange3(self._expr_0,currValue); self._expr_0 = currValue; } }; Wrapper_ChildComponent.prototype.ngDoCheck = function(view,el,throwOnChange) { var self = this; var changed = self._changed; self._changed = false; if (!throwOnChange) { if (changed) { self.context.ngOnChanges(self._changes); jit_setBindingDebugInfoForChanges4(view.renderer,self._changes); self._changes = {}; } } return changed; }; ... return Wrapper_ChildComponent })
我有话说
1.注册指令生命周期钩子时,一定要实现对应的接口么 ?
注册指令生命周期钩子时,实现对应的接口不是必须的,接口可以帮助我们在开发阶段尽早地发现错误,因为我们有可能在注册生命周期钩子的时候,写错了某个钩子的名称,在运行时可能不会抛出任何异常,但页面显示却不是预期的效果,因此建议读者还是遵守该开发规范。另外还要注意的一点是,TypeScript 中定义的接口,是不会编译生成 ES5 相关代码,它只用于编译阶段做校验。
未完待续