4.认识Angular组件之2

11. 变化监测:Angular提供了数据绑定的功能.所谓的数据绑定就是将组件类的数据和页面的DOM元素关联起来.当数据发生变化时,Angular能够监测到这些变化,并对其所绑定的DOM元素

进行相应的更新,反之亦然.

异步事件的发生会导致组件中的数据变化,但Angular并不是捕捉对象的变化,它采用的是在适当的时机去检验对象的值是被改动.这个时机是由NgZone这个服务去掌控的,它获取到了整个

应用的执行上下文,能够对相关的异步事件发生,完成或者异常等进行捕获,然后驱动Angular的变化监测机制执行.

11.1 数据变化的源头:在应用程序当中,大致有这三种引起数据变化的应用场景.

1.用户的行为操作,即页面操作所引发的用户事件,如click,changes,hover等.

2.前后端的数据交互,如从后端服务拉取页面所需的接口数据,如XMLHttpRequest/WebSocket等.

3.各类定时任务,即在某个延时后再来响应对应的操作.从而对页面数据做出改变,如setTimeout,setInterval,requestAnimationFrame等.

以上三种可能导致数据变化的情景有一个共同的特征,即它们都是异步的处理,使用异步回调函数句柄来处理相关数据操作.因此,任意一个异步操作,都有可能在数据层面上发生改变,

这回导致应用程序的状态被改变.如果可以在每一个异步回调函数执行结束后,通知Angular内核进行变化监测,那么任何数据的更改就可以在视图层实时额反馈出来.

11.2 变动通知机制:通过异步事件来通知Angular进行变化监测,让任何数据的变更可以被实时的反映出来.Angular本身不具备捕获异步事件的机制,通过引入NgZone服务来实现.

NgZone是基于Zone来实现的,NgZone从Zone中fork了一份实例,是Zone派生出的一个子Zone,在Angular环境内注册的异步事件都运行在这个子Zone上.

Zone是如何具备异步事件捕获能力的?Zone以同样的接口,不同的实现方式并替换了一系列与JavaScript的事件相关的标准方法.因此当开发者使用标准接口时,实际上会显调用Zone

的替换方法,再由这些替换方法调用底层的标准方法.这种对上层应用透明的设计,使得在引入Zone的时候,原有代码不需要做太大的改动.

NgZone扩展了一些API并添加了一些可以被订阅的自定义事件,这些自定义事件是Observable流:

1.onUnstable:在Angular单次事件启动前,触发消息通知订阅器.

2.onMicrotaskEmpty:在Zone完成当前Angular单次事件任务时,立即通知订阅者.

3.onStable:在完成onMicrotaskEmpty回调函数之后,在视图变化之前立即通知订阅者,常用来验证应用程序的状态.

由于NgZone只是全局Zone的一个fork,Angular能够决定在Zone内需不需要执行变化监测,如NgZone的runOutsideAngular()方法可以让Angular不执行变化监测.

runOutsideAnglar():即通知NgZone的父Zone在捕获到异步事件时直接返回,从而不在触发自定义的onMicrotaskEmpty事件,直接作用就是不在通知Angular执行变化监测.

针对上面的说明,对变动通知机制可作详细阐述,如:当有异步事件触发导致数据变化时,这些异步事件会被Zones捕获并触发onUnstable自定义事件,在该自定义事件绑定的函数中

来通知Angular去执行变化监测,如当鼠标经的mousemove事件发生时,它将触发变化通知监测.

11.3 变化监测的处理机制:Angular应用由大大小小的组件组成,这些有相互依赖关系的组件组成了一个线性的组件树.此外,没个组件都有一个自己的变化检测器,由此组成的变化检测树.

变化监测树的数据是由上到下单向流动,变化监测的执行总是从根组件开始,从上到下的监测每一个组件的变化.

当一个异步事件发生并导致其中组件数据的改变,在组件中绑定的相关处理事件将会被触发,事件句柄(对象)处理完成相关逻辑之后,NgZone将会执行对应的钩子函数并通知Angular

去执行一次变化监测.

默认情况下,任何一个组件模型中的数据变化都会导致整个组件树的变化监测,但是有很多组件的输入属性是没有变化的,因此没有必要对这些组件进行变化监测操作.减少不必要的监测

操作可以提升应用程序的性能.

11.4 变化监测类:Angular在整个运行期间都会为每个组件创建变化检测类的实例,该实例提供了相关的方法来手动管理变化监测.由于Angular并不知道是哪个组件发生了变化,但是开发者

知道,所以可以给这个组件做一个标记,以此来通知Angular仅仅监测这个组件所在路径上的组件即可.

变化监测类ChangeDelectorRef提供的主要接口如下:

1.markForCheck():把根组件到该组件之间的这条路径标记起来,通知Angular在下次触发变化监测时必须检查这条路径上的组件.

2.detach():从变化监测树中分离变化监测器,该组件的变化监测器将不在执行变化监测,除非再次手动执行reattact().

3.reattact():把分离的变化监测器重新安装上,使得该组件机器子组件都能执行变化检测.

4.detectChanges():手动触发执行该组件到各个子组件的一次变化监测.

示例如下:

@Component({

selector:'list',

template:`

<ul>

<li *ngFor="let contact of contacts">

<list-item [contact]="contact"></list-item>

</li>

</ul>

`

})

export class ListComponent implements OnInit{

contacts:any={};

//构造器的参数用了语法糖,可以快捷创建一个属cd并绑定到构造函数参数上.

contructor(private cd:ChangeDetectorRef){

//首先将该组件(包含其子组件)从变化监测树中排除出去

cd.detach();

//定时手动执行变化监测,即:每个5秒手动触发一次该组件及其子组件的Angular变化监测

setInterval(()=>{ //这里简化了contacts数据来源代码

this.cd.detectChanges();

},5000);

}

ngOnInit(){

this.getContacts();

}

getContacts(){

this.contacts=data; //这里简化了contacts数据来源代码

}

}

11.4 变化监测策略:@Component中有个可选的元数据changeDetection,它的作用是让开发者定义每个组件的变化监视策略.在使用该功能前,需要先导入ChangeDetectionStrategy

对象.ChangeDetectionStrategy枚举类型值有两种:

1.Default:组件的每次变化监测都会检查其内部所有数据(引用对象会被深度遍历),以此得出数据前后变化;

2.OnPush:组件的变化监测只检查输入属性(@Input修饰的变量)的值是否发生改变,当这个值是引用类型(如Object,Array等)时,则仅对比引用是否改变.

OnPush策略相比Default降低了变化监测的复杂度,对性能提升更好,但是OnPush策略只对比值的'引用',这在某些场景中可能会得不到预期的效果,如果希望组件也能正常更新数

据,解决的办法有两个:

1.使用Default策略,但会牺牲性能.xzm

2.使用Immutable对象来传值,这是比较推荐的做法.

使用Immutable对象可以确保当对象值的引用地址不变时,对象内部的值或结构也会保持不变.反之,当对象内部发生变化时,对象的引用必然发生改变.

示例如下:

//子组件代码

import { Component,Input,ChangeDetectionStragety } from '@angular/core';

@Component({

selector:'list-item',

template:`

<div>

<label> {{contact.get('name')}} </lable>

<span>{{ contact.get('telNum') }}</span>

</div>

`,

changeDetection:ChangeDetectionStragety.OnPush

})

export class ListItemComponet{

@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:ChangeDetectionStragety.OnPush

})

export classListComponent{

contactItem:any;

constructor(){

this.contactItem=Immutable.map({

name:'xzm',

telNum:'12345678'

});

}

}

12 组件的其他元数据:

12.1 host:是个功能强大的元数据,主要是用在指令中.通过host指令,可以指定此指令/组件的事件,动作和属性等.

12.2 exportAs:主要是在指令中使用,作用是将指令分配给一个变量,相当于别名.

12.3 moduleId:包含该组件模块的id,它被用于解析模板和样式的相对路径.在Dart中它可以被自动确定,在CommonJS中,它总是被设置为module.id,这中情况下,如果CSS,HTML,TypeScript

文件在同一目录,如app下,则可以去除基准路径,如"app/".

示例:

@Component({

moduleId:module.id,

templateUrl:'some.component.html',

styleUrls:['some.component.css']

})

选择Webpack方案,可以采用"./"开头的相对路径写法,webpack会自动调用require()方法来加载这些模板与样式.

12.4 queries:设置需要被注入到组件的查询.在组件中主要有两种查询,即视图查询和内容查询,它们分别会在ngAfterViewInit和ngAfterContentInit回调函数被调用之前设置.

1.视图查询示例:

//包装一个输入框成组件,实现一单被渲染,则获取焦点.

@Component({

selector:'my-input',

template:`

<input #theInput type="text" />

`,

queries:{

input:new ViewChild('theInput')

}

})

export class MyInput implements AfterViewInit{

input : ElemenetRef=null;

constructor(private renderer:Renderer){}

ngAfterViewInit(){

this.renderer.invokeElementMethod(this.input.nativeElement,'focus');

}

}

以上代码其实相当于如下代码:

@Component({

selector:'my-input',

template:`

<input #theInput type="text" />

`

})

export class MyInput implements AfterViewInit{

@ViewChild('theInput') input:ElemenetRef;

constructor(private renderer:Renderer){}

ngAfterViewInit(){

this.renderer.invokeElementMethod(this.input.nativeElement,'focus');

}

}

2.内容查询:和视图查询类似,不过内容查询是配和ng-content使用的.

示例:

<my-list>

<li *ngFor="#item of items">{{item}}</li>

</my-list>

@Directive({selector:'li'})

export class ListItem{}

@Component({

selector:'my-list',

template:`

<input #theInput type="text" />

`,

queries:{

items:new ContentChildrenn(ListItem) //通过ListItem的选择器绑定li元素

}

})

export class MyInput implements AfterContentInit{

items:new QueryList<ListItem></ListItem>;

constructor(private renderer:Renderer){}

ngAfterContentInit(){

}

}

12.5 animations:animations元数据提供了便捷的动画定义方法,使用方式就和自定义标签一样.animations元数据定义需要先从@angular/core引入一些用于动画的函数,

如下:import {

trigger,

state,

style,

transition,

animate

} from '@angular/core';

定义一个按钮状态动画效果,有"on"和"off"两种状态,默认是"on",点击按钮切换状态,颜色变红,字变小(1.2~1).

示例代码如下:

//...

animations:[

trigger("buttonStatus",[

state('on',style( { color:'#of2', transform:'scale(1.2)' } )),

state('off',style( { color:'f00',transform:'scale(1)' } )),

transition('off=>on',animate('100ms ease-in')),

transition('on=>off',animate('100ms ease-out'))

])

]

有了以上定义的动画效果,就可以在组件模板上通过@triggerName的方式来应用到元素当中,如:

//...

template:`

<div>

<button @buttonStatus="status" (click)="toggleStatus">{{status}}</button>

</div>

`

//...

export class example {

status:string='on';

toggleStatus(){

this.status=(this.status==='on') ? 'off' : 'on';

}

}