什么是雙向綁定
如圖:

雙向綁定機制維護了頁面(View)與數據(Data)的一致性。如今,MVVM已經是前段流行框架必不可少的一部分。
Angular2中的雙向綁定
雙向綁定,也是Angular2的核心概念之一,Angular2的雙向綁定是這樣的:
- data=>view:數據綁定,模板語法是 []
- view=>data:事件綁定,模板語法是 ()
- Angular其實並沒有一個雙向綁定的實現,他的雙向綁定就是數據綁定+事件綁定,模板語法是 [()] 。
Angular2官方給的例子:
<!--value是數據綁定,input是事件綁定--> <input [value]="currentHero.name" (input)="currentHero.name=$event.target.value" > <!--等價--> <input [(ngModel)]="currentHero.name">
上面是input空間的雙向綁定語法,很清楚的說明了雙向綁定與兩個單向綁定的關系。這里沒有使用ngModule
語法,ngModule
語法內部實現與這個差不多。
事件綁定
- 用戶操作出發DOM事件通知
- Angular監聽到了通知,然后執行模板語法,上面的例子就是將input控件的輸入值賦給了
currentHero.name
。
數據綁定
由於js語言並沒有屬性變化通知的機制,所以angular也不知道誰發生了變化,在什么時候變了。Angular的變化機制是:

上面的例子中input的數據綁定過程如下:
- 代碼修改了
currentHero.name
的值。 - 觸發整個組件樹的變化檢查。
- input顯示了修改后的值。
數據何時變化
主要入下集中情況可能改變數據:
- 用戶輸入操作,比如點擊,提交等。
- 請求服務端數據。
- 定時事件,比如
setTimeout
,setInterval
。
這幾點有個共同點,就是他們都是異步的。也就是說,所有的異步操作是可能導致數據變化的根源因素。
如何通知變化
在Angularjs中是由代碼$scope.$apply()
或者$scope.$digest
觸發,而Angular2接入了ZoneJS
,由它監聽了Angular所有的異步事件。ZoneJS重寫了所有的異步API(所謂的猴子補丁,MonkeyPath)。ZoneJS會通知Angular可能有數據發生變化,需要檢測更新。
變化檢測原理 -- 臟檢查
所謂臟檢查就是存儲所有變量的值,每當可能有變量發生變化需要檢查時,就將所有變量的舊值跟新值進行比較,不相等就說明檢測到變化,需要更新對應的視圖。
AngularJS與Angular2變化檢測的區別
Angularjs的變化檢測機制也是臟檢查,而Angular2的變化檢測性能比Angularjs提升了很多。
Angular2
Angular的核心是組件化,組件的嵌套會使得最終形成一棵組件樹。Angular的變化檢測可以分組件進行,每個組件都有對應的變化檢測器ChangeDetector
。可想而知,這些變化檢測器也會構成一棵樹。
另外,Angular的數據流是自頂而下的,從父組件到子組件單向流動。單向數據流向保證了高效、可預測的變化檢測,盡管檢查了負組件之后,自組件可能會改變父組件的數據使得父組件需要再次被檢查,這是不被推薦的數據處理方式。在開發模式下,Angular會進行二次檢查,如果出現上述情況,二次檢查就會報錯:ExpressionChangedAfterItHasBeenCheckedError
(關於這個問題的答案,可以在參考資料中找到)。而在生產環境中,臟檢查只會執行一次。
Angularjs
相比之下,Angularjs采用的是雙向數據流,錯綜復雜的數據流使得他不得不多次檢查,使得數據最終趨向穩定。理論上,數據永遠不可能穩定,Angularjs的策略是,臟檢查超過10次就認定程序有問題。

變化檢測優化
優化策略
有2個思路:
- OnPush策略:我知道我沒變,別查我。
- 手動控制刷新:我變了,只查我。
變化檢測策略 OnPush
Angular還讓開發者擁有制定變化策略的能力。
export enum ChangeDetectionStrategy { OnPush, // 表示變化檢測對象的狀態為`CheckOnce` Default, // 表示變化檢測對象的狀態為`CheckAlways` }
從ChangeDetectionStrategy
可以看到,Angular有兩種變化檢測策略。Default
是Angular默認的變化檢測策略,也就是臟檢查(只要有值發生變化,就全部檢查)。開發者可以根據場景來設置更加高效的變化檢測方式:OnPush
。OnPush
策略,就是只有當輸入數據的引用發生變化或者有事件觸發時,組件進行變化檢測。
@Component({ template: ` <h2>{{vData.name}}</h2> <span>{{vData.email}}</span> `, // 設置該組件的變化檢測策略為onPush changeDetection: ChangeDetectionStrategy.OnPush }) class VCardCmp { @Input() vData; }
比如上面這個例子,當vData
的屬性值發生變化的時候,這個組件不會發生變化檢測,只有當vData
重新賦值的時候才會。一般,只接受輸入的木偶子組件(dumb components)比較適合采用onPush
策略。
那什么時候只要對象的屬性值發生變化,整個對象的引用就變了呢?不可變對象(Immutable Object)。當組件中的輸入對象是不變量時,可采用onPush
變化檢測策略,減少變化檢測的頻率。換個角度來說,為了更加智能地執行變化檢測,可以在只接受輸入的子組件中采用onPush
策略。
手動控制變化檢測
Angular不僅可以讓開發者設置變化檢測策略,還可以讓開發者獲取變化檢測對象引用ChangeDetectorRef
,手動去操作變化檢測。變化檢測對象引用給開發者提供的方法有以下幾種:
markForCheck()
:將檢查組件的所有父組件所有子組件,即使設置了變化檢測策略為onPush
。detach()
:將變化檢測對象脫離檢測對象樹,不再進行變化檢查;結合detectChanges
可實現局部變化檢測。(采用onPush
策略之后的組件detach()
無效)detectChanges()
:將檢測該組件及其子組件,結合detach
可實現局部檢測。checkNoChanges()
: 檢測該組件及其子組件,如果有變化存在則報錯,用於開發階段二次驗證變化已經完成。reattach()
:將脫離的變化檢測對象重新鏈接到變化檢測樹上。
那么,如果是Observable的話,它會訂閱所有的變量變化,只要在訂閱回調函數中手動觸發變化檢測即可實現最小成本的檢測(仍采用onPush
變化檢測策略)。舉個例子:
@Component({ template: '{{counter}}', changeDetection: ChangeDetectionStrategy.OnPush }) class CartBadgeCmp { @Input() addItemStream:Observable<any>; counter = 0; constructor(private cd: ChangeDetectorRef) {} ngOnInit() { this.addItemStream.subscribe(() => { this.counter++; // 數據模型發生變化 this.cd.markForCheck(); // 手動觸發檢測 }) } }
另外,當數據模型變化太過頻繁,我們可自定義變化檢測的時機。舉個例子:
@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(); // 脫離變化檢測樹 } } }
總結
Angular與Angularjs都采用變化檢測機制,前者優於后者主要體現在:
- 單項數據流動
- 以組件為單位維度獨立進行檢測
- 生產環境只進行一次檢查
- 可自定義的變化檢測策略:
Default
和onPush
- 可自定義的變化檢測操作:
markForcheck()
、detectChanges()
、detach()
、reattach()
、checkNoChanges()
- 代碼實現上的優化,據說采用了VM friendly的代碼。
鏈接:https://www.jianshu.com/p/cee44e8831c9