1. 程式人生 > 實用技巧 >Angular資料雙向繫結

Angular資料雙向繫結

來源:https://www.cnblogs.com/jingwhale/p/5117419.html

Angular資料雙向繫結

AngularJS誕生於2009年,由Misko Hevery 等人建立,後為Google所收購。是一款優秀的前端JS框架,已經被用於Google的多款產品當中。AngularJS有著諸多特性,最為核心的是:MVVM、模組化、自動化雙向資料繫結、語義化標籤、依賴注入等等。

一.什麼是資料雙向繫結

Angular實現了雙向繫結機制。所謂的雙向繫結,無非是從介面的操作能實時反映到資料,資料的變更能實時展現到介面。

一個最簡單的示例就是這樣:

<div ng-controller="CounterCtrl">
    <span ng-bind="counter"></span>
    <button ng-click="counter++">increase</button>
</div>function CounterCtrl($scope) {
    $scope.counter = 1;
}

這個例子很簡單,每當點選一次按鈕,介面上的數字就增加一。

二.資料雙向繫結原理

1.深入理解

實現使用者控制手機列表顯示順序的特性。動態排序可以這樣實現,新增一個新的模型屬性,把它和迭代器整合起來,然後讓資料繫結完成剩下的事情。

模板(app/index.html)
Search: <input ng-model="query">
Sort by:
<select ng-model="orderProp">
  <option value="name">Alphabetical</option>
  <option value="age">Newest</option>
</select>


<ul class="phones">
  <li ng-repeat="phone in phones | filter:query | orderBy:orderProp">
    {{phone.name}}
    <p>{{phone.snippet}}</p>
  </li>
</ul>

在index.html中做了如下更改:

首先,增加了一個叫做orderProp的<select>標籤,這樣使用者就可以選擇提供的兩種排序方法。

然後,在filter過濾器後面新增一個orderBy過濾器用其來處理進入迭代器的資料。orderBy過濾器以一個數組作為輸入,複製一份副本,然後把副本重排序再輸出到迭代器。
AngularJS在select元素和orderProp模型之間建立了一個雙向繫結。而後,orderProp會被用作orderBy過濾器的輸入。

什麼時候資料模型發生了改變(比如使用者在下拉選單中選了不同的順序),AngularJS的資料繫結會讓檢視自動更新。沒有任何笨拙的DOM操作。

控制器(app/js/controllers.js)
function PhoneListCtrl($scope) {
  $scope.phones = [
    {"name": "Nexus S",
     "snippet": "Fast just got faster with Nexus S.",
     "age": 0},
    {"name": "Motorola XOOM™ with Wi-Fi",
     "snippet": "The Next, Next Generation tablet.",
     "age": 1},
    {"name": "MOTOROLA XOOM™",
     "snippet": "The Next, Next Generation tablet.",
     "age": 2}
  ];

  $scope.orderProp = 'age';
}

修改了phones模型—— 手機的陣列 ——為每一個手機記錄其增加了一個age屬性。根據age屬性來對手機進行排序。
在控制器程式碼里加了一行讓orderProp的預設值為age。如果我們不設定預設值,這個模型會在使用者在下拉選單選擇一個順序之前一直處於未初始化狀態。

現在我們該好好談談雙向資料綁定了。注意到當應用在瀏覽器中載入時,“Newest”在下拉選單中被選中。這是因為我們在控制器中把orderProp設定成了‘age’。所以繫結在從我們模型到使用者介面的方向上起作用——即資料從模型到檢視的繫結。現在當你在下拉選單中選擇“Alphabetically”,資料模型會被同時更新,並且手機列表陣列會被重新排序。這個時候資料繫結從另一個方向產生了作用——即資料從檢視到模型的繫結。

2.原理分析

下面的原理想法實際上很基礎,可以被認為是3步走計劃:

我們需要一個UI元素和屬性相互繫結的方法

我們需要監視屬性和UI元素的變化

我們需要讓所有繫結的物件和元素都能感知到變化

還是有很多方法能夠實現上面的想法,有一個簡單有效的方法就是使用PubSub模式。 這個思路很簡單:我們使用資料特性來為HTML程式碼進行繫結,所有被繫結在一起的JavaScript物件和DOM元素都會訂閱一個PubSub物件。只要JavaScript物件或者一個HTML輸入元素監聽到資料的變化時,就會觸發繫結到PubSub物件上的事件,從而其他繫結的物件和元素都會做出相應的變化。

3.釋出者-訂閱者模式(PubSub模式)

設計該模式背後的主要動力是促進形成鬆散耦合。在這種模式中,並不是一個物件呼叫另一個物件的方法,而是一個物件訂閱另一個物件的特定活動並在狀態改變後獲得通知。訂閱者也稱為觀察者,而補觀察的物件稱為釋出者或主題。當發生了一個重要的事件時,釋出者將會通知(呼叫)所有訂閱者並且可能經常以事件物件的形式傳遞訊息。

假設有一個釋出者paper,它每天出版報紙及月刊雜誌。訂閱者joe將被通知任何時候所發生的新聞。

該paper物件需要一個subscribers屬性,該屬性是一個儲存所有訂閱者的陣列。訂閱行為只是將其加入到這個陣列中。當一個事件發生時,paper將會迴圈遍歷訂閱者列表並通知它們。通知意味著呼叫訂閱者物件的某個方法。故當使用者訂閱資訊時,該訂閱者需要向paper的subscribe()提供它的其中一個方法。

paper也提供了unsubscribe()方法,該方法表示從訂閱者陣列(即subscribers屬性)中刪除訂閱者。paper最後一個重要的方法是publish(),它會呼叫這些訂閱者的方法,總而言之,釋出者物件paper需要具有以下這些成員:

①subscribers 一個數組

②subscribe() 將訂閱者新增到subscribers陣列中

③unsubscribe() 從subscribers陣列中刪除訂閱者

④publish() 迴圈遍歷subscribers陣列中的每一個元素,並且呼叫他們註冊時所提供的方法

所有這三種方法都需要一個type引數,因為釋出者可能觸發多個事件(比如同時釋出一本雜誌和一份報紙)而使用者可能僅選擇訂閱其中一種,而不是另外一種。

由於這些成員對於任何釋出者物件都是通用的,故將它們作為獨立物件的一個部分來實現是很有意義的。那樣我們可將其複製到任何物件中,並將任意給定物件變成一個釋出者。

三.用jQuery做一個簡單的實現

對於DOM事件的訂閱和釋出,用jQuery實現起來是非常簡單的,接下來我們就是用Jquery比如下面:

function DataBinder( object_id ) {
  // Use a jQuery object as simple PubSub
  var pubSub = jQuery({});
 
  // We expect a `data` element specifying the binding
  // in the form: data-bind-<object_id>="<property_name>"
  var data_attr = "bind-" + object_id,
      message = object_id + ":change";
 
  // Listen to change events on elements with the data-binding attribute and proxy
  // them to the PubSub, so that the change is "broadcasted" to all connected objects
  jQuery( document ).on( "change", "[data-" + data_attr + "]", function( evt ) {
    var $input = jQuery( this );
 
    pubSub.trigger( message, [ $input.data( data_attr ), $input.val() ] );
  });
 
  // PubSub propagates changes to all bound elements, setting value of
  // input tags or HTML content of other tags
  pubSub.on( message, function( evt, prop_name, new_val ) {
    jQuery( "[data-" + data_attr + "=" + prop_name + "]" ).each( function() {
      var $bound = jQuery( this );
 
      if ( $bound.is("input, textarea, select") ) {
        $bound.val( new_val );
      } else {
        $bound.html( new_val );
      }
    });
  });
 
  return pubSub;
}

對於上面這個實現來說,下面是一個User模型的最簡單的實現方法:

function User( uid ) {
  var binder = new DataBinder( uid ),
 
      user = {
        attributes: {},
 
        // The attribute setter publish changes using the DataBinder PubSub
        set: function( attr_name, val ) {
          this.attributes[ attr_name ] = val;
          binder.trigger( uid + ":change", [ attr_name, val, this ] );
        },
 
        get: function( attr_name ) {
          return this.attributes[ attr_name ];
        },
 
        _binder: binder
      };
 
  // Subscribe to the PubSub
  binder.on( uid + ":change", function( evt, attr_name, new_val, initiator ) {
    if ( initiator !== user ) {
      user.set( attr_name, new_val );
    }
  });
 
  return user;
}

現在我們如果想要將User模型屬性繫結到UI上,我們只需要將適合的資料特性繫結到對應的HTML元素上。

// javascript
var user = new User( 123 );
user.set( "name", "Wolfgang" );
 
// html
<input type="number" data-bind-123="name" />

這樣輸入值會自動對映到user物件的name屬性,反之亦然。到此這個簡單實現就完成了。

四.Angular實現資料雙向繫結

Angular主要通過scopes實現資料雙向繫結。AngularJS的scopes包括以下四個主要部分:

digest迴圈以及dirty-checking,包括watch,watch,digest,和$apply。
Scope繼承 - 這項機制使得我們可以建立scope繼承來分享資料和事件。
對集合 – 陣列和物件 – 的有效dirty-checking。
事件系統 -onon,emit,以及$broadcast。

我們主要講解第一條Angular資料繫結是怎麼實現的。

1.digest迴圈以及dirty-checking,包括watch,watch,digest,和$apply

①瀏覽器事件迴圈和Angular.js擴充套件

我們的瀏覽器一直在等待事件,比如使用者互動。假如你點選一個按鈕或者在輸入框裡輸入東西,事件的回撥函式就會在javascript直譯器裡執行,然後你就可以做任何DOM操作,等回撥函式執行完畢時,瀏覽器就會相應地對DOM做出變化。 Angular拓展了這個事件迴圈,生成一個有時成為angular context的執行環境(這是個重要的概念)。

watchwatch佇列(watch list)

每次你繫結一些東西到你的UI上時你就會往$watch佇列裡插入一條$watch。想象一下$watch就是那個可以檢測它監視的model裡時候有變化的東西。

當我們的模版載入完畢時,也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular直譯器會尋找每個directive,然後生成每個需要的$watch

③$digest迴圈

還記得我前面提到的擴充套件的事件迴圈嗎?當瀏覽器接收到可以被angular context處理的事件時,digestevalAsyncdigest迴圈就會觸發。這個迴圈是由兩個更小的迴圈組合起來的。一個處理evalAsync佇列,另一個處理watch佇列。 這個是處理什麼的呢?digestdigest將會遍歷我們的watch,然後詢問它是否有屬性和值的變化,直$watch佇列都檢查過。

這就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問了:有沒有$watch更新過?如果有至少一個更新過,這個迴圈就會再次觸發,直到所有的$watch都沒有變化。這樣就能夠保證每個model都已經不會再變化。記住如果迴圈超過10次的話,它將會丟擲一個異常,防止無限迴圈。 當$digest迴圈結束時,DOM相應地變化。

例如:controllers.js
app.controller('MainCtrl', function() {
  $scope.name = "Foo";

  $scope.changeFoo = function() {
      $scope.name = "Bar";
  }
});

index.html

{{ name }}
<button ng-click="changeFoo()">Change the name</button>

這裡我們有一個$watch因為ng-click不生成$watch(函式是不會變的)。

    • 我們按下按鈕
    • 瀏覽器接收到一個事件,進入angular context(後面會解釋為什麼)。
    • $digest迴圈開始執行,查詢每個$watch是否變化。
    • 由於監視$scope.name$watch報告了變化,它會強制再執行一次$digest迴圈。
    • 新的$digest迴圈沒有檢測到變化。
    • 瀏覽器拿回控制權,更新與$scope.name新值相應部分的DOM。

這裡很重要的(也是許多人的很蛋疼的地方)是每一個進入angular context的事件都會執行一個$digest迴圈,也就是說每次我們輸入一個字母迴圈都會檢查整個頁面的所有$watch

④通過$apply來進入angular context

誰決定什麼事件進入angular context,而哪些又不進入呢?$apply!

如果當事件觸發時,你呼叫applyangularcontext調調apply,它會進入angularcontext,如果沒有呼叫就不會進入。現在你可能會問:剛才的例子裡我也沒有呼叫apply,為什麼?Angular為你做了!因此你點選帶有ng-click的元素時,時間就會被封裝到一個apply調ngmodel="foo"f調apply呼叫。如果你有一個ng−model="foo"的輸入框,然後你敲一個f,事件就會這樣呼叫apply("foo = 'f';")。

Angular什麼時候不會自動為我們applyAngularjQuery西jQuery調apply呢?這是Angular新手共同的痛處。為什麼我的jQuery不會更新我繫結的東西呢?因為jQuery沒有呼叫apply,事件沒有進入angular context,$digest迴圈永遠沒有執行。

2.具體實現

AngularJS的scopes就是一般的JavaScript物件,在它上面你可以繫結你喜歡的屬性和其他物件,然而,它們同時也被添加了一些功能用於觀察資料結構上的變化。這些觀察的功能都由dirty-checking來實現並且都在一個digest迴圈中被執行。

①Scope 物件

建立一個test/scope_spec.js檔案,並將下面的測試程式碼新增到其中:

test/scope_spec.js  
-------
/* jshint globalstrict: true */    
/* global Scope: false */
'use strict';
describe("Scope", function() {
it("can be constructed and used as an object", function() { 
var scope = new Scope();
scope.aProperty = 1;
    expect(scope.aProperty).toBe(1);
  });
});

這個測試用來建立一個Scope,並在它上面賦一個任意值。我們可以輕鬆的讓這個測試通過:建立src/scope.js檔案然後在其中新增以下內容:

src/scope.js  
------
/* jshint globalstrict: true */
'use strict'; function Scope() {
}

在這個測試中,我們將一個屬性(aProperty)賦值給了這個scope。這正是Scope上的屬性執行的方式。它們就是正常的JavaScript屬性,並沒有什麼特別之處。這裡你完全不需要去呼叫一個特別的setter,也不需要對你賦值的型別進行什麼限制。真正的魔法在於兩個特別的函式:watchwatch和digest。我們現在就來看看這兩個函式。

②監視物件屬性:watchwatch和digest

watchwatch和digest是同一個硬幣的兩面。它們二者同時形成了$digest迴圈的核心:對資料的變化做出反應。

為了實現這一塊功能,我們首先來定義一個測試檔案並斷言你可以使用watch調watch來註冊一個監視器,並且當有人呼叫了digest的時候監視器的監聽函式會被呼叫。

在scope_spec.js檔案中新增一個巢狀的describe塊。並建立一個beforeEach函式來初始化這個scope,以便我們可以在進行每個測試時重複它:

test/scope_spec.js   
------  
describe("Scope", function() {
it("can be constructed and used as an object", function() { var scope = new Scope();
scope.aProperty = 1;
    expect(scope.aProperty).toBe(1);
  });
describe("digest", function() {
var scope;
beforeEach(function() { scope = new Scope();
});
it("calls the listener function of a watch on first $digest", function() { var watchFn = function() { return 'wat'; };
var listenerFn = jasmine.createSpy();
scope.$watch(watchFn, listenerFn);
      scope.$digest();
      expect(listenerFn).toHaveBeenCalled();
}); });
});

在上面的這個測試中我們呼叫了watchscope便JasmineSpy調watch來在這個scope上註冊一個監視器。我們現在對於監視函式本身並沒有什麼興趣,因此我們隨便提供了一個函式來返回一個常數值。作為監聽函式,我們提供了一個JasmineSpy。接著我們呼叫了digest並檢查這個監聽器是否真正被呼叫。

首先,這個Scope需要有一些地方去儲存所有被註冊的監視器。我們現在就在Scope建構函式中新增一個數組儲存它們:

src/scope.js
-----

function Scope(){
    this.$$watchers = [];
}

上面程式碼中的$$字首在AngularJS框架中被認為是私有變數,它們不應該在應用的外部被呼叫。

現在我們可以來定義watchwatch函數了。它接收兩個函式作為引數,並且將它們儲存在$watchers陣列中。我們想要每一個Scope物件都擁有這個函式,因此我們將它新增到Scope的原型中:

src/scope.js   
-----
Scope.prototype.$watch = function(watchFn, listenerFn) {
 var watcher = {
    watchFn: watchFn,
    listenerFn: listenerFn
  };
this.$$watchers.unshift(watcher); 
};

最後我們應該有一個digestdigest函式。現在,我們來定義一個digest函式的簡化版本,它僅僅只是會迭代所有的註冊監視器並呼叫它們的監聽函式:

digest能夠持續的迭代所有監視函式,直到被監視的值停止變化。多做幾次digest是我們能夠獲得運用於監視器並依賴於其他監視器的變化。

首先,我們新建名為$$digestOnce,並且調整它以便它能夠在所有監視器上執行一遍,然後返回一個布林值來說明有沒有任何變化:

src/scope.js 
----

Scope.prototype.$$digestOnce = function(){
    var length = this.$$watchers.length;
    var watcher, newValue, oldValue, dirty;
    while(length--){
        watcher = this.$$watchers[length];
        newValue = watcher.watchFn(this);
        oldValue= watcher.last;
        if(newValue !== oldValue){
            watcher.last == newValue;
            watcher.listenerFn(newValue, oldValue, this);
            dirty = true;
        }
    }
     return dirty;
};

接著,我們重定義digest便調digest以便它能夠執行“外迴圈”,在變化發生時呼叫$digestOnce:

src/scope.js
-----
Scope.prototype.$digest = function(){
    var dirty;
    do {
        dirty = this.$$digestOnce();
    } while (dirty);
};

很多人對Angular的髒檢測機制感到不屑,推崇基於setter,getter的觀測機制,在我看來,這只是同一個事情的不同實現方式,兩者是各有優劣。