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,$digest,和$apply。
Scope繼承 - 這項機制使得我們可以創建scope繼承來分享數據和事件。
對集合 – 數組和對象 – 的有效dirty-checking。
事件系統 - $on,$emit,以及$broadcast。
我們主要講解第一條Angular數據綁定是怎么實現的。
1.digest循環以及dirty-checking,包括$watch,$digest,和$apply
①瀏覽器事件循環和Angular.js擴展
我們的瀏覽器一直在等待事件,比如用戶交互。假如你點擊一個按鈕或者在輸入框里輸入東西,事件的回調函數就會在javascript解釋器里執行,然后你就可以做任何DOM操作,等回調函數執行完畢時,瀏覽器就會相應地對DOM做出變化。 Angular拓展了這個事件循環,生成一個有時成為angular context的執行環境(這是個重要的概念)。
②$watch 隊列($watch list)
每次你綁定一些東西到你的UI上時你就會往$watch隊列里插入一條$watch
。想象一下$watch
就是那個可以檢測它監視的model里時候有變化的東西。
當我們的模版加載完畢時,也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular解釋器會尋找每個directive,然后生成每個需要的$watch
。
③$digest循環
還記得我前面提到的擴展的事件循環嗎?當瀏覽器接收到可以被angular context處理的事件時,$digest循環就會觸發。這個循環是由兩個更小的循環組合起來的。一個處理evalAsync隊列,另一個處理$watch隊列。 這個是處理什么的呢?$digest將會遍歷我們的$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!
如果當事件觸發時,你調用$apply,它會進入angular context,如果沒有調用就不會進入。現在你可能會問:剛才的例子里我也沒有調用$apply,為什么?Angular為你做了!因此你點擊帶有ng-click的元素時,時間就會被封裝到一個$apply調用。如果你有一個ng-model="foo"的輸入框,然后你敲一個f,事件就會這樣調用$apply("foo = 'f';")。
Angular什么時候不會自動為我們$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,也不需要對你賦值的類型進行什么限制。真正的魔法在於兩個特別的函數:$watch和$digest。我們現在就來看看這兩個函數。
②監視對象屬性:$watch和$digest
$watch和$digest是同一個硬幣的兩面。它們二者同時形成了$digest循環的核心:對數據的變化做出反應。
為了實現這一塊功能,我們首先來定義一個測試文件並斷言你可以使用$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(); }); }); });
在上面的這個測試中我們調用了$watch來在這個scope上注冊一個監視器。我們現在對於監視函數本身並沒有什么興趣,因此我們隨便提供了一個函數來返回一個常數值。作為監聽函數,我們提供了一個Jasmine Spy。接着我們調用了$digest並檢查這個監聽器是否真正被調用。
首先,這個Scope需要有一些地方去存儲所有被注冊的監視器。我們現在就在Scope構造函數中添加一個數組存儲它們:
src/scope.js ----- function Scope(){ this.$$watchers = []; }
上面代碼中的$$前綴在AngularJS框架中被認為是私有變量,它們不應該在應用的外部被調用。
現在我們可以來定義$watch函數了。它接收兩個函數作為參數,並且將它們儲存在$$watchers數組中。我們想要每一個Scope對象都擁有這個函數,因此我們將它添加到Scope的原型中:
src/scope.js ----- Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.unshift(watcher); };
最后我們應該有一個$digest函數。現在,我們來定義一個$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以便它能夠運行“外循環”,在變化發生時調用$$digestOnce:
src/scope.js ----- Scope.prototype.$digest = function(){ var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };
很多人對Angular的臟檢測機制感到不屑,推崇基於setter,getter的觀測機制,在我看來,這只是同一個事情的不同實現方式,兩者是各有優劣。
-------------------------------------------------------------------------------------------------------------------------------------
完
轉載需注明轉載字樣,標注原作者和原博文地址。
更多閱讀: