Angularjs與Angular2雙向綁定原理

1、angularjs(1.x)數據雙向綁定原理

AngularJs 爲 scope 模型上設置了一個 監聽隊列,用來監聽數據變化並更新 view 。每次綁定一個東西到 view(html) 上時 AngularJs 就會往 watchers 隊列裏插入一條watch,用來檢測它監視的 model 裏是否有變化的東西。當瀏覽器接收到可以被 angular context 處理的事件時,digest 循環就會觸發。digest 會遍歷所有的 $watch。從而更新DOM。

watch 這有點類似於我們的觀察者模式,在當前作用域scope下,我們創建一個監控器watchers和一個監聽器watch,watchers 負責管理所有的watch,當我們每次綁定到UI上的時候就自動創建一個watch,並把它放到watchers。

注:即便我們在scope上添加了兩個變量,但是隻有一個綁定在了UI上,因此在這裏只生成了一個watch。

digest 當瀏覽器接收到可以被angular context處理的事件時,digest循環就會觸發。digest將會遍歷我們的watch,如果watch沒有變化,這個循環檢測就將停止,如果有至少一個更新過,這個循環就會再次觸發,直到所有的watch都沒有變化。這樣就能夠保證每個model都已經不會再變化。這就是髒檢查(Dirty Checking)機制。

applyapply 我們可以直接理解爲刷新UI。如果當事件觸發時,你調用apply,它會進入angular context,如果沒有調用就不會進入,之後的digest檢測機制就不會觸發。

總結:

  • 只有在scope變量綁定到頁面上,纔會創建watch
  • $apply決定事件是否可以進入angular context
  • $digest 循環檢查model時最少兩次,最多10次(多於10次拋出異常,防止無限檢查)
  • AngularJs自帶的指令已經實現了$apply,所以不需要我們額外的編寫
  • 在自定義指令時,建議使用帶function參數的$apply

2、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語法內部實現與這個差不多。

事件綁定

  1. 用戶操作出發DOM事件通知
  2. Angular監聽到了通知,然後執行模板語法,上面的例子就是將input控件的輸入值賦給了currentHero.name

數據綁定

由於js語言並沒有屬性變化通知的機制,所以angular也不知道誰發生了變化,在什麼時候變了。Angular的變化機制是:

上面的例子中input的數據綁定過程如下:

  1. 代碼修改了currentHero.name的值。
  2. 觸發整個組件樹的變化檢查。
  3. input顯示了修改後的值。
數據何時變化

主要入下集中情況可能改變數據:

  • 用戶輸入操作,比如點擊,提交等。
  • 請求服務端數據。
  • 定時事件,比如setTimeoutsetInterval

這幾點有個共同點,就是他們都是異步的。也就是說,所有的異步操作是可能導致數據變化的根源因素。

如何通知變化

在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個思路:

  1. OnPush策略:我知道我沒變,別查我。
  2. 手動控制刷新:我變了,只查我。

變化檢測策略 OnPush

Angular還讓開發者擁有制定變化策略的能力。

export enum ChangeDetectionStrategy { 
  OnPush, // 表示變化檢測對象的狀態爲`CheckOnce` 
  Default, // 表示變化檢測對象的狀態爲`CheckAlways`
}

ChangeDetectionStrategy可以看到,Angular有兩種變化檢測策略。Default是Angular默認的變化檢測策略,也就是髒檢查(只要有值發生變化,就全部檢查)。開發者可以根據場景來設置更加高效的變化檢測方式:OnPushOnPush策略,就是隻有當輸入數據的引用發生變化或者有事件觸發時,組件進行變化檢測。

@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(); // 脫離變化檢測樹
      }
  }
}

Angular2與Angularjs比較

Angular2與Angularjs都採用變化檢測機制,前者優於後者主要體現在:

  • 單項數據流動
  • 以組件爲單位維度獨立進行檢測
  • 生產環境只進行一次檢查
  • 可自定義的變化檢測策略:DefaultonPush
  • 可自定義的變化檢測操作:markForcheck()detectChanges()detach()reattach()checkNoChanges()
  • 代碼實現上的優化,據說採用了VM friendly的代碼。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章