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