博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

4.认识Angular组件之2

Posted on 2017-02-05 19:23  徐自勉  阅读(1858)  评论(0编辑  收藏  举报

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';
}
}