@Component({selector: 'test',template: `<div>张三</div>`})export class ContentItem(){}
- 组件装饰器:每个被@Component进行修饰才能成为Angular组件
- 组件元数据:selector、template
- 模板:每个组件都会关联一个模板,模板会最终渲染到页面上,页面的这个DOM元素是此组件的宿主元素
- 组件类,普通的类,逻辑都在这里面定义并实现
2.1 组件装饰器
@Component 是TS语法,它是一个装饰器,任何Angular组件类都会用这个装饰器修饰,如果移除了这个装饰器,它就不再是Angular组件。
组件元数据
- 用于定义标签名的selector,定义匹配的标签,组件的命名标记。采用小字母并以’-‘分隔。
- 用于定义组件宿主元素内敛模板template,还提供了外联模板templateUrl。每个组件只能指定一个模板。建议使用外联,代码结构清晰易管理。
- 内联样式styles, 外联样式styleUrls。这个可以同时指定,styles会被先解析,才解析外联的,styleUrls会覆盖styles的样式,如果直接在DOM上写样式,它的优先级最高。建议使用外联,代码结构清晰易管理。
组件与模块
通常组件不会独立存在,而通过与其他组件协作,完成一个完整的功能特性。在Angular中,这样的功能特性通常会封装到一个模块里。
模块是组件在上一层的抽象,组件以及指令、管道、服务、路由等都能通过模块去组织。
模块的构成
Angular提供了@NgModule装饰器来创建模块,一个应用可以有多个模块,但有且只有一个根模块(Root Module)、其他模块叫作特性模块(Feature Module)。根模块是启动应用的入口模块,必须通过bootstrap元数据来指定应用的根组件,然后通过bootstrapModule()方法来启动应用。
@NgModule({imports: [BrowserModule],declarations: [ContactItemComponent],bootstrap: [ContactItemComponent]})export class AppModule{}
最后创建一个app.ts,利用platformBrowserDynamic().bootstrapModule()来启动根模块,这样angular应用就能运行起来。
NgModule元数据
- declarations: 用于指定属于这个模块的视图类,即指定哪些部件组成了这个模块。Angular有组件、指令和管道三种视图,这些视图只能属于一个模块,要注意不要再次声明属于其他模块的类。
- exports 导出是视图类。当该模块被引入外部模块的时,这个属性指定了外部模块可以使用该模块的哪些类视图,它的值类型跟declarations一致
- imports: 引入该模块依赖的其他模块或路由,引入后模块里的组件模板才能引用外部对应的组件、指令、管道
- providers: 指令模块依赖的服务,引入后该模块中的所有组件都可以使用这些服务。
2.2 组件交互
父组件向子组件传递数据
父组件
@Compoent({selector: 'list',template: `<ul class="list"><li *ngFor="let contact of contacts"><list-item [contact]="contact"></list-item></li></ul>`})export class ListComponent implements OnInit {this.contacts = data;}
子组件
@Compoent({selector: 'list-item',template: `<div class="contact-info"><label class="contact-name">{{ contact.name }}</label><span class="contact-tel">{{ contact.telNum }}</span></div>`})export class ListItemComponent implements OnInit {@Input() contacts:any = {};}
使用@Input接收从父组件传递过来的数据。
子组件可以进行拦截输入属性的数据并相应的处理。使用setter拦截和ngOnChanges监听数据变化。
setter拦截输入属性
@Compoent({selector: 'list-item',template: `<div class="contact-info"><label class="contact-name">{{ contact.name }}</label><span class="contact-tel">{{ contact.telNum }}</span></div>`})export class ListItemComponent implements OnInit {_contact: object = {};@Input()set contactObj(contact: object) {this._contact.name = (contact.name && contact.name.trim()) || 'no name set';this._contact.telNum = contact.telNum || '000-0000'};get contactObj() { return this._contact; }}
ngOnChanges监听数据变化
用于响应Angular绑定属性中发生的数据变化,该方法接收一个数据对象,包含当前值和变化前的值。在ngOnInit之前,或者当数据绑定的输入属性的值发生变化时会触发。它时生命周期的钩子函数。
父组件
@Component({selector: 'detail',template: `<a class="edit" (click)="editContact()">编辑</a><change-log [contact]="detail"></change-log>`})export class DetailComponent implements OnInit {detail: any = {}editContact(){this.detail = data;}}
SimpleChanges类。是Angular的基础类,用于处理数据前后变化,其中包含两个变量,分别是previousValue和currentValue,preiousValues是获取数据变化前的数据,而currentValue是获取变化后的数据。
@Component({selector: 'change-log',template: `<ul><li *ngFor="let change of changes">{{ change }}</li></ul>`})export class ChangeLogComponent implements OnChange {@Input contart = {};changes: string[] = [];ngOnChanges(changes: {[propKey: string]: SimpleChanges}) {let log: string[] = [];for(let propName in changes) {let changedProp = changes[propName].from = JSON.stringify(changedProp.previousValue),to = JSON.stringify(changedProp.currentValue);log.push(`${propName} changed from ${from} to ${to}`)}this.changes.push(log.join(', '))}}
在子组件中,通过ngOnChanges钩子方法来监测数据变化前后的情况。ngOnChanges当且仅当组件输入数据变化时被调用。
子组件向父组件传递数据
使用事件传递。子组件需要实例化一个用来订阅和触发自定义事件的EventEmitter类,这个实例对象是一个由装饰器@Output修饰的输出属性,当用户操作行为发生时该事件会触发,父组件则通过事件绑定的方式来订阅来自子组件触发的事件,即子组件触发的具体事件会被其父组件订阅到
@Component({selector: 'collection',template: `<contact-collect [contact]="detail" (onCollect)="collectTheContact($event)"></contact-collect>`})export class CollectionComponent implements OnInit {detail: any = {}collectTheContact(){this.detail.collection == 0 ? this.detail.collection = 1 : this.detail.collection = 0;}}
父组件通过绑定自定义事件onCollect订阅来自子组件的触发事件。
@Component({selector: 'contact-collect',template: `<i [ngClass]="{ collected: contact.collection }" (click)="collectTheContact()">收藏</i>`})export class ContactCollectComponent implements OnInit {@Input() contact: any = {};@Output() onCollect = new EventEmitter<boolean>();collectTheContact(){this.onCollect.emit();}}
通过属性@Output将数据流向父组件,在父组件完成事件监听,从而实现子组件到父组件的交互。
其他组件交互方式
- 父组件通过局部变量获取子组件的引用
-
通过局部变量实现数据交互
在Angular中通过”模板局部变量”,可以帮助我们获取子组件的实例引用。在父组件模板中为子组件创建一个局部变量,那么这个父组件可以通过这个局部变量来获取子组件公共成员变量和函数的权限。
父组件@Component({selector: 'collection',template: `<contact-collect (click)="collect.collectTheContact()" #collect></contact-collect>`})export class CollectionComponent implements OnInit {}
子组件
@Component({selector: 'contact-collect',template: `<i [ngClass]="{ collected: contact.collection }" >收藏</i>`})export class ContactCollectComponent implements OnInit {detail = {};collectTheContact() {this.detail.collection == 0? this.detail.collection = 1: this.detail.collection = 0;}}
使用@ViewChild实现数据交互
组件中元数据ViewChild的作用是声明对子组件元素的实例引用,它提供了一个参数来选择将要引用的组件元素,这个参数可以是一个类的实例,也可以是一个字符串。
参数为类实例,表示父组件将绑定一个指令或子组件实例
- 参数为字符串类型,表示将起到选择器的作用,即相当于在父组件中绑定一个模板局部变量,获取到子组件的一份实例对象的引用。
@Component({selector: 'collection',template: `<contact-collect (click)="collectTheContact()"></contact-collect>`})export class CollectionComponent {@ViewChild(ContactCollectComponent) contactCollect: ContactCollectComponent;ngAfterViewInit(){}collectTheContact(){this.contactCollect.collectTheContact();}}
2.3 组件内容嵌入
使用@Component({selector: 'example-content',template: `<div><div><ng-content select=""header></ng-content></div></div>`})export class ExampleContentComponent {}
example中的内容会填充到,ExampleContentComponent模板中ng-content的位置。我们可以通过select来匹配需要显示的内容,ng-content上的select属性是一个选择器,与CSS作用是类似的。@Component({selector: 'app',template: `<example-content><header>123</header></example-content>`})export class ContentAppComponent {}
2.4组件的生命周期
组件生命周期由Angular内部管理,从组件的创建、渲染、到数据变动事件的触发,再到组件从DOM移除,Angular都提供了一系列钩子。很方便的在这些钩子触发的时候,执行相应的回调函数。
2.4.1生命周期钩子
开发者可以实现一个或者多个生命周期钩子。这些钩子包含在@angular/core中,每一个接口都对应一个名为’ng + 接口名’的方法。
- ngOnChanges
- ngOnInit
- ngDoCheck
- ngAfterContentInit
- ngAfterContentChecked
- ngAfterViewInit
- ngAfterViewChecked
- ngOnDestroy
有的组件还提供了自己特有的生命周期钩子,例如路由有routerOnActivate。
ngOnChanges
响应组件输入值发生变化时触发的事件。该方法接收一个SimpleChanges对象,包含当前值和变化前的值。该方法在ngOnInit之前,或者当绑定输入属性的值发生变化时会触发。
ngOnInit
ngOnInit用于数据绑定输入属性之后初始化组件。该钩子会在第一次ngOnChanges之后被调用。
- 组件构造后不久就要进行复杂的初始化
- 需要在输入属性设置完成之后才构建组件
经常使用ngOnInit获取数据。为什么不在构造函数中获取数据呢。首先,构造函数做的事,例如成员变量初始化,应该尽可能简单。对于Angular自动化测试的一些场景也有非常重要的作用,把业务相关的初始化代码放到ngOnInit里可以很容易进行hook操作,而构造函数不能被显式调用,因此无法进行Hook操作。
ngDoCheck
用于变化监测,该钩子方法会在每次变化检测发生时调用。
每一个变化监测周期内,不管数据值是否发生了变化,ngDoCheck都会被调用。这个钩子要慎用,例如鼠标移动时会触发mousemove,此时变化监测会被频繁触发,随之ngDoCheck也会被频繁调用。因此,ngDoCheck方法中不能写一些复杂的代码,否则性能会受到影响。
绝大多数情况下,ngDoCheck和ngOnChanges不应该一起使用。ngOnChanges能做的事情,ngDoCheck也能做到,而且ngDoCheck监测的粒度更小,可以完成更灵活的变化监测逻辑。
ngAfterContentInit
在组件使用
ngAfterContentChecked
在组件使用了
ngAfterViewInit
ngAfterViewInit会在Angular创建了组件的视图及其子视图之后被调用。
ngAfterViewChecked
ngAfterViewChecked在Angular创建子组件的视图及其子组件视图之后被调用一次,并且在每次子组件变化监测时也会被调用。
ngOnDestroy
ngOnDestroy在销毁指令/组件之前触发。那些不会被垃圾回收器自动回收的资源(比如已订阅的观察者事件,绑定过的DOM事件、通过setTimeout或setInterval设置过的计时器等等)都应当在ngOnDestory中手动销毁,从为避免内存泄露的问题。
2.5 变化监测
Angular提供了数据绑定的功能。所谓数据绑定就是将组件类的数据和页面DOM元素关联起来。当数据发生变化时,Angular能够监测到这些变化,并对其所绑定的DOM元素进行相应的更新,反之亦然。
异步事件发生会导致组件中数据的变化,但Angular并不是捕捉对象的变动,它采用的是在适当时机去检查对象的值是否被改动。这个时机由NgZone这个服务去掌控的,它获取整个应用的执行上下文,能够对异步事件发生、完成或异常等进行捕获,然后驱动Angular的变化监测机制执行。
2.5.1 数据变化的源头
大概有三种引起数据变化的应用场景。
- 用户的行为操作。例如click,change,hover,对用户的操作做出响应;
- 前后端的数据交互
-
2.5.2 变动通知机制
NgZone是基于Zones来实现的。在Angular环境内注册的异步事件都运行在这个子Zone上(这个Zone拥有Angular运行环境的执行上下文)。NgZone拓展了一些API添加了一些功能性方法。这些功能性的方法称为钩子。当有异步操作发生、完成或抛出异常时、会有对应的钩子对其捕获,处理一些操作。
NgZone提供一些可被订阅的自定义事件,这些自定义事件是Observable流 onUnstable: 在Angular单次事件启动前,触发消息通知订阅器
- onMicrotaskEmpty: 在Zone完成当前Angular单次事件任务、立刻通知订阅者
- onState: 在完成onMicrotaskEmpty回调函数之后,在视图变化之前立即通知订阅者,常用来验证应用程序的状态。
NgZone提供的这些自定义事件在跟踪定时任务和其他异步任务时非常有用。由于NgZone其实只是全局Zone的一个fork,Angular能够决定了Zone内不需要执行变化监测,例如NgZone的runOutsideAngular()方法可以让Angular不执行变化监测。我们并不总是希望Angular每一次都去执行变化监测,具体会在下面的下节详细讲解。
runOutsideAngular(): 即通知Ngzone在捕获到异步事件时直接返回,从而不再触发自定义onMicrotaskEmpty事件,直接作用就不在通知Angular执行变化监测。
当有异步事件触发导致数据变化,这些异步事件会被Zones捕获并触发onUnstable自定义事件,在自定义事件绑定的函数中来通知Angular去执行变化监测,如鼠标经过mousemove事件发生时,将触发变化监测。
ApplicationRef类的简单讲解来帮助我们理解NgZone。该类的构造函数中监听NgZone中的onMicrotaskEmpty自定义事件,只有任何异步任务发生将触发这个事件,其中的tick()方法是用来通知Angular去执行变化监测
class ApplicationRef {changeDetectorRefs: ChangeDetectorRef[] = [];constructor(private zone: NgZone) {this.zone.onMicrotaskEmpty.subscribe(() => this.zone.run(() => this.tick())}tick() {this.changeDetectorRefs.forEach((ref) => ref.detectChanges());}}
2.5.3 变化监测的响应处理
Angular由大大小小的组件组成,这些有相互依赖关系的组件组成了一棵线性的组件树。此外,每一个组件都有自己的变化监测器,由此组成了一棵变化监测树。变化监测树的数据是由上到下单向流动,这是因为变化监测的执行总是由根组件开始,从上到下地监测每一个组件的变化。单向的数据流让人清晰地了解视图中数据的来源,明白数据的变化是由哪个组件引起的。
在联系人列表中,由ListComponent组件获取到联系人列表数据,在其子组件ListItemComponent展示具体的联系人数据。当一个异步事件发生并导致其中组件数据的改变,在组件中绑定的相关处理事件将会触发,事件句柄处理完成相关逻辑之后,NgZone将会执行对应的钩子函数并通知Angular去执行一次变化监测。
组件树的每一个组件都有对应的变化监测器。这使得每个组件的变化监测相互独立,可以更灵活地控制变化监测地执行或暂停等,对提升性能有重要的意义。
那么在这个组件树中,变化监测器是如何工作的呢?当组件中数据有变动时,NgZone通过钩子捕获到变化并通知Angular去执行变化监测。变化监测是单向性的,即从根组件开始,Angular都会创建一个变化监测类的实例,该实例能准确地记录每个组件地数据模型,并一次作为下一轮变化监测地参考标准。
默认情况下,任何一个组件模型中地数据变化都会导致整个组件树地变化监测,但其实很多组件地输入属性是没有变化的,因此没必要对这样的组件进行变化监测操作。减少不必要的监测操作可以提升程序性能,了解变化检测类。
2.5.4 变化检测类
Angular在整个运行期间都会为每一个组件创建变化监测类的实例,该实例提供了相关的方法来手动管理变化监测。当发生变化时,Angular会从根组件到子组件来监测每个组件是否发生了变化。Angular并不知道那个组件发生了变化,但开发者知道,所以可以给这个组件做标记,以此来通知Angular仅仅监测这个组件所在路径上的组件即可
主要接口
class ChangeDetectorRef {markForCheck(): void;detach(): void;detectChanges(): void;reacttach(): void;}
- markForCheck 把根组件把该组件之间的这条路径标记起来,通知Angular在下次触发变化监测时必须检查这条路径上的组件
- detach 从变化监测树中分离变化检测器,该组件的变化监测器将不再执行变化监测,除非再次手动执行reacttach
- reacttach 把分离的变化监测器重新安装上,使得该组件及其子组件都能执行变化监测
- detectChanges手动触发执行该组件到各个子组件的一次变化监测
假设通讯录中联系人的数据时刻在变化,而产品需求又不需要实时地根据变化来展示数据,那么为了性能的考虑,可以设置在一定时间内来执行变化监测。这里我们通过detach()和detectChanges()方法配合使用来实现性能的优化
@Component({selector: 'list',template: `<ul class="list"><li *ngFor="let contact of contacts"><list-item [contact]="contact"></list-item></li></ul>`})export class ListComponent implements OnInit {contacts: any = {};constructor(private cd: ChangeDetectorRef) {cd.detach();setInterval(() => {this.cd.detectChanges();}, 5000)}ngOnInit() {this.getContacts();}getContacts() {this.contacts = data;}}
detach方法用于从当前组件分离变化监测器,在需要变化监测的时候在通过detectChanges手动触发变化监测。在列表数据变化频繁的情况下,这样处理方法能起到很大的优化作用。
2.5.5 变化监测策略
元数据 changeDetection,它的作用是让开发者定义每个组件的变化监测策略。在使用该功能前,需要先导入ChangeDetectionStrategy对象
import { Component, ChangeDetectionStrategy } from '@angular/core'@Component({changeDetection: ChangeDetectionStrategy.OnPush})export class ContactComponent {}
ChangeDetectionStrategy枚举类型值有两种,分别是Default和OnPush。当值为Default,组件的每次变化监测都会检查内部的所有数据(引用对象也会被深度遍历),以此得出前后数据得变化;当值为OnPush,组件得变化监测只检查输入属性(@Input)的值是否发生变化,当值为引用类型时,则只对比该值的引用。
OnPush策略降低了变化监测的复杂度,很好地提升了变化监测的性能,如果子组件的更新只依赖输入属性的值,那么子组件使用OnPush策略是一个很好的选择。当OnPush策略只对比值的”引用”,某些场景下可能会得不到预期效果。例如子组件获取父组件的一个Object值,{a: 1, b: 2},当父组件修改了{a: 11, b: 2},这时候对象的引用没有发生变化,因此子组件的变化监测并不能感知到对象已变化,方法。
- 修改为Default, 牺牲性能
- 使用Immutable来传值,这是比较推荐的做法
使用Immutable对象可以确保当对象值得引用地址不变时,对象内部得值或结构也会保持不变。当对象内部发生变化时,对象的引用必然会发生改变。
子组件
import { Component, Input, ChangeDetectionStrategy } from '@angular/core'@Component({selector: 'list-item',template: `<div><label>{{ contact.get('name') }}</label><span>{{ contact.get('telNum') }}</span></div>`,changeDetection: ChangeDetectionStrategy.OnPush})export class ListItemComponent {@Input() contact: any = {};}
父组件
import { Component } from '@angular/core';import Immutable from 'immutable';@Component({// ...template: `<list-item [contact]="contactItem"></list-item><button (click)="doUpdate()">更新</button>`,changeDetection: ChangeDetectionStrategy.OnPush})export class ListComponent {contactItem: any;constructor() {this.contactItem = Immutable,map({name: 'zs',telNum: '12345678'})}doUpdate() {this.contactItem = this.contactItem.set('telNum', '87654321')}}
2.6 扩展阅读
元数据
Encapsulation 组件的视图包装,主要目的 让组件间的样式更加独立而互不影响,使得组件的复用变得简单
- ViewEncapsulation.None: 无Shadow DOM 且无样式包装,所有样式都会应用到整个document
- ViewEncapsulation.Emulated: 无Shadow DOM,但通过Angular提供的样式包装机制来模拟组件的独立性,使得组件的样式不受外部影响,Angular默认样式。组件间相互独立,互不影响。
- ViewEncapsulation.Native: 使用原生的Shadow DOM特性
