前幾天總結(jié)了MVC闸英、MVP、MVVM設(shè)計(jì)模式介袜,其中MVVM的核心機(jī)制就是雙向綁定甫何。React、Vue遇伞、Angular的雙向綁定辙喂,都是基于MVVM的設(shè)計(jì)模式。
什么是雙向綁定
如圖:
雙向綁定機(jī)制維護(hù)了頁面(View)與數(shù)據(jù)(Data)的一致性鸠珠。如今加派,MVVM已經(jīng)是前段流行框架必不可少的一部分。
Angular2中的雙向綁定
雙向綁定跳芳,也是Angular2的核心概念之一芍锦,Angular2的雙向綁定是這樣的:
- data=>view:數(shù)據(jù)綁定,模板語法是 []
- view=>data:事件綁定飞盆,模板語法是 ()
- Angular其實(shí)并沒有一個(gè)雙向綁定的實(shí)現(xiàn)娄琉,他的雙向綁定就是數(shù)據(jù)綁定+事件綁定,模板語法是 [()] 吓歇。
Angular2官方給的例子:
<!--value是數(shù)據(jù)綁定孽水,input是事件綁定-->
<input [value]="currentHero.name"
(input)="currentHero.name=$event.target.value"
>
<!--等價(jià)-->
<input [(ngModel)]="currentHero.name">
上面是input空間的雙向綁定語法,很清楚的說明了雙向綁定與兩個(gè)單向綁定的關(guān)系城看。這里沒有使用ngModule
語法女气,ngModule
語法內(nèi)部實(shí)現(xiàn)與這個(gè)差不多。
事件綁定
- 用戶操作出發(fā)DOM事件通知
- Angular監(jiān)聽到了通知测柠,然后執(zhí)行模板語法炼鞠,上面的例子就是將input控件的輸入值賦給了
currentHero.name
缘滥。
數(shù)據(jù)綁定
由于js語言并沒有屬性變化通知的機(jī)制,所以angular也不知道誰發(fā)生了變化谒主,在什么時(shí)候變了朝扼。Angular的變化機(jī)制是:
上面的例子中input的數(shù)據(jù)綁定過程如下:
- 代碼修改了
currentHero.name
的值。 - 觸發(fā)整個(gè)組件樹的變化檢查霎肯。
- input顯示了修改后的值擎颖。
數(shù)據(jù)何時(shí)變化
主要入下集中情況可能改變數(shù)據(jù):
- 用戶輸入操作,比如點(diǎn)擊观游,提交等搂捧。
- 請求服務(wù)端數(shù)據(jù)。
- 定時(shí)事件懂缕,比如
setTimeout
异旧,setInterval
。
這幾點(diǎn)有個(gè)共同點(diǎn)提佣,就是他們都是異步的吮蛹。也就是說,所有的異步操作是可能導(dǎo)致數(shù)據(jù)變化的根源因素拌屏。
如何通知變化
在Angularjs中是由代碼$scope.$apply()
或者$scope.$digest
觸發(fā)潮针,而Angular2接入了ZoneJS
,由它監(jiān)聽了Angular所有的異步事件。ZoneJS重寫了所有的異步API(所謂的猴子補(bǔ)丁倚喂,MonkeyPath)每篷。ZoneJS會(huì)通知Angular可能有數(shù)據(jù)發(fā)生變化,需要檢測更新端圈。
變化檢測原理 -- 臟檢查
所謂臟檢查就是存儲(chǔ)所有變量的值焦读,每當(dāng)可能有變量發(fā)生變化需要檢查時(shí),就將所有變量的舊值跟新值進(jìn)行比較舱权,不相等就說明檢測到變化矗晃,需要更新對應(yīng)的視圖。
AngularJS與Angular2變化檢測的區(qū)別
Angularjs的變化檢測機(jī)制也是臟檢查宴倍,而Angular2的變化檢測性能比Angularjs提升了很多张症。
Angular2
Angular的核心是組件化,組件的嵌套會(huì)使得最終形成一棵組件樹鸵贬。Angular的變化檢測可以分組件進(jìn)行俗他,每個(gè)組件都有對應(yīng)的變化檢測器ChangeDetector
±疲可想而知兆衅,這些變化檢測器也會(huì)構(gòu)成一棵樹。
另外,Angular的數(shù)據(jù)流是自頂而下的羡亩,從父組件到子組件單向流動(dòng)摩疑。單向數(shù)據(jù)流向保證了高效、可預(yù)測的變化檢測夕春,盡管檢查了負(fù)組件之后未荒,自組件可能會(huì)改變父組件的數(shù)據(jù)使得父組件需要再次被檢查专挪,這是不被推薦的數(shù)據(jù)處理方式及志。在開發(fā)模式下,Angular會(huì)進(jìn)行二次檢查寨腔,如果出現(xiàn)上述情況速侈,二次檢查就會(huì)報(bào)錯(cuò):ExpressionChangedAfterItHasBeenCheckedError
(關(guān)于這個(gè)問題的答案,可以在參考資料中找到)迫卢。而在生產(chǎn)環(huán)境中倚搬,臟檢查只會(huì)執(zhí)行一次。
Angularjs
相比之下乾蛤,Angularjs采用的是雙向數(shù)據(jù)流每界,錯(cuò)綜復(fù)雜的數(shù)據(jù)流使得他不得不多次檢查,使得數(shù)據(jù)最終趨向穩(wěn)定家卖。理論上眨层,數(shù)據(jù)永遠(yuǎn)不可能穩(wěn)定,Angularjs的策略是上荡,臟檢查超過10次就認(rèn)定程序有問題趴樱。
變化檢測優(yōu)化
優(yōu)化策略
有2個(gè)思路:
- OnPush策略:我知道我沒變,別查我酪捡。
- 手動(dòng)控制刷新:我變了叁征,只查我。
變化檢測策略 OnPush
Angular還讓開發(fā)者擁有制定變化策略的能力逛薇。
export enum ChangeDetectionStrategy {
OnPush, // 表示變化檢測對象的狀態(tài)為`CheckOnce`
Default, // 表示變化檢測對象的狀態(tài)為`CheckAlways`
}
從ChangeDetectionStrategy
可以看到捺疼,Angular有兩種變化檢測策略。Default
是Angular默認(rèn)的變化檢測策略永罚,也就是臟檢查(只要有值發(fā)生變化帅涂,就全部檢查)。開發(fā)者可以根據(jù)場景來設(shè)置更加高效的變化檢測方式:OnPush
尤蛮。OnPush
策略媳友,就是只有當(dāng)輸入數(shù)據(jù)的引用發(fā)生變化或者有事件觸發(fā)時(shí),組件進(jìn)行變化檢測产捞。
@Component({
template: `
<h2>{{vData.name}}</h2>
<span>{{vData.email}}</span>
`,
// 設(shè)置該組件的變化檢測策略為onPush
changeDetection: ChangeDetectionStrategy.OnPush
})
class VCardCmp {
@Input() vData;
}
比如上面這個(gè)例子醇锚,當(dāng)vData
的屬性值發(fā)生變化的時(shí)候,這個(gè)組件不會(huì)發(fā)生變化檢測,只有當(dāng)vData
重新賦值的時(shí)候才會(huì)焊唬。一般恋昼,只接受輸入的木偶子組件(dumb components)比較適合采用onPush
策略。
那什么時(shí)候只要對象的屬性值發(fā)生變化赶促,整個(gè)對象的引用就變了呢液肌?不可變對象(Immutable Object)鸥滨。當(dāng)組件中的輸入對象是不變量時(shí)嗦哆,可采用onPush
變化檢測策略婿滓,減少變化檢測的頻率。換個(gè)角度來說凸主,為了更加智能地執(zhí)行變化檢測橘券,可以在只接受輸入的子組件中采用onPush
策略。
手動(dòng)控制變化檢測
Angular不僅可以讓開發(fā)者設(shè)置變化檢測策略旁舰,還可以讓開發(fā)者獲取變化檢測對象引用ChangeDetectorRef
,手動(dòng)去操作變化檢測嗡官。變化檢測對象引用給開發(fā)者提供的方法有以下幾種:
-
markForCheck()
:將檢查組件的所有父組件所有子組件箭窜,即使設(shè)置了變化檢測策略為onPush
谨湘。 -
detach()
:將變化檢測對象脫離檢測對象樹,不再進(jìn)行變化檢查紧阔;結(jié)合detectChanges
可實(shí)現(xiàn)局部變化檢測坊罢。(采用onPush
策略之后的組件detach()
無效) -
detectChanges()
:將檢測該組件及其子組件,結(jié)合detach
可實(shí)現(xiàn)局部檢測活孩。 -
checkNoChanges()
: 檢測該組件及其子組件,如果有變化存在則報(bào)錯(cuò)乖仇,用于開發(fā)階段二次驗(yàn)證變化已經(jīng)完成憾儒。 -
reattach()
:將脫離的變化檢測對象重新鏈接到變化檢測樹上乃沙。
那么,如果是Observable的話警儒,它會(huì)訂閱所有的變量變化训裆,只要在訂閱回調(diào)函數(shù)中手動(dòng)觸發(fā)變化檢測即可實(shí)現(xiàn)最小成本的檢測(仍采用onPush
變化檢測策略)眶根。舉個(gè)例子:
@Component({
template: '{{counter}}',
changeDetection: ChangeDetectionStrategy.OnPush
})
class CartBadgeCmp {
@Input() addItemStream:Observable<any>;
counter = 0;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
this.addItemStream.subscribe(() => {
this.counter++; // 數(shù)據(jù)模型發(fā)生變化
this.cd.markForCheck(); // 手動(dòng)觸發(fā)檢測
})
}
}
另外边琉,當(dāng)數(shù)據(jù)模型變化太過頻繁,我們可自定義變化檢測的時(shí)機(jī)变姨。舉個(gè)例子:
@Component({
template: `{{counter}}
<input type="check" (click)="toggle()">`,
})
class CartBadgeCmp {
counter = 0;
detectEnabled = false;
constructor(private cd: ChangeDetectorRef) {}
ngOnInit() {
// 每10毫秒增加1
setInterval(()=>{this.counter++}, 10);
}
toggle(){
if( this.detectEnabled ){
this.cd.reattach(); // 鏈接上變化檢測樹
}
else{
this.cd.detach(); // 脫離變化檢測樹
}
}
}
總結(jié)
Angular與Angularjs都采用變化檢測機(jī)制族扰,前者優(yōu)于后者主要體現(xiàn)在:
- 單項(xiàng)數(shù)據(jù)流動(dòng)
- 以組件為單位維度獨(dú)立進(jìn)行檢測
- 生產(chǎn)環(huán)境只進(jìn)行一次檢查
- 可自定義的變化檢測策略:
Default
和onPush
- 可自定義的變化檢測操作:
markForcheck()
定欧、detectChanges()
渔呵、detach()
忧额、reattach()
厘肮、checkNoChanges()
- 代碼實(shí)現(xiàn)上的優(yōu)化睦番,據(jù)說采用了VM friendly的代碼。