理解Angular數(shù)據(jù)雙向綁定
AngularJS是一款優(yōu)秀的前端JS框架,已經(jīng)被用于Google的多款產(chǎn)品當(dāng)中。AngularJS有著諸多特性,最為核心的是:MVVM、模塊化、自動化雙向數(shù)據(jù)綁定、語義化標(biāo)簽、依賴注入等等。
一.什么是數(shù)據(jù)雙向綁定
Angular實現(xiàn)了雙向綁定機(jī)制。所謂的雙向綁定,無非是從界面的操作能實時反映到數(shù)據(jù),數(shù)據(jù)的變更能實時展現(xiàn)到界面。
一個最簡單的示例就是這樣:
<div ng-controller="CounterCtrl"> <span ng-bind="counter"></span> <button ng-click="counter++">increase</button> </div>function CounterCtrl($scope) { $scope.counter = 1; }
這個例子很簡單,每當(dāng)點擊一次按鈕,界面上的數(shù)字就增加一。
二.數(shù)據(jù)雙向綁定原理
1.深入理解
實現(xiàn)用戶控制手機(jī)列表顯示順序的特性。動態(tài)排序可以這樣實現(xiàn),添加一個新的模型屬性,把它和迭代器集成起來,然后讓數(shù)據(jù)綁定完成剩下的事情。
模板(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>標(biāo)簽,這樣用戶就可以選擇提供的兩種排序方法。
然后,在filter過濾器后面添加一個orderBy過濾器用其來處理進(jìn)入迭代器的數(shù)據(jù)。orderBy過濾器以一個數(shù)組作為輸入,復(fù)制一份副本,然后把副本重排序再輸出到迭代器。
AngularJS在select元素和orderProp模型之間創(chuàng)建了一個雙向綁定。而后,orderProp會被用作orderBy過濾器的輸入。
什么時候數(shù)據(jù)模型發(fā)生了改變(比如用戶在下拉菜單中選了不同的順序),AngularJS的數(shù)據(jù)綁定會讓視圖自動更新。沒有任何笨拙的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模型—— 手機(jī)的數(shù)組 ——為每一個手機(jī)記錄其增加了一個age屬性。根據(jù)age屬性來對手機(jī)進(jìn)行排序。
在控制器代碼里加了一行讓orderProp的默認(rèn)值為age。如果我們不設(shè)置默認(rèn)值,這個模型會在用戶在下拉菜單選擇一個順序之前一直處于未初始化狀態(tài)。
現(xiàn)在我們該好好談?wù)勲p向數(shù)據(jù)綁定了。注意到當(dāng)應(yīng)用在瀏覽器中加載時,“Newest”在下拉菜單中被選中。這是因為我們在控制器中把orderProp設(shè)置成了‘a(chǎn)ge'。所以綁定在從我們模型到用戶界面的方向上起作用——即數(shù)據(jù)從模型到視圖的綁定?,F(xiàn)在當(dāng)你在下拉菜單中選擇“Alphabetically”,數(shù)據(jù)模型會被同時更新,并且手機(jī)列表數(shù)組會被重新排序。這個時候數(shù)據(jù)綁定從另一個方向產(chǎn)生了作用——即數(shù)據(jù)從視圖到模型的綁定。
2.原理分析
下面的原理想法實際上很基礎(chǔ),可以被認(rèn)為是3步走計劃:
- 我們需要一個UI元素和屬性相互綁定的方法
- 我們需要監(jiān)視屬性和UI元素的變化
- 我們需要讓所有綁定的對象和元素都能感知到變化
還是有很多方法能夠?qū)崿F(xiàn)上面的想法,有一個簡單有效的方法就是使用PubSub模式。 這個思路很簡單:我們使用數(shù)據(jù)特性來為HTML代碼進(jìn)行綁定,所有被綁定在一起的JavaScript對象和DOM元素都會訂閱一個PubSub對象。只要JavaScript對象或者一個HTML輸入元素監(jiān)聽到數(shù)據(jù)的變化時,就會觸發(fā)綁定到PubSub對象上的事件,從而其他綁定的對象和元素都會做出相應(yīng)的變化。
3.發(fā)布者-訂閱者模式(PubSub模式)
設(shè)計該模式背后的主要動力是促進(jìn)形成松散耦合。在這種模式中,并不是一個對象調(diào)用另一個對象的方法,而是一個對象訂閱另一個對象的特定活動并在狀態(tài)改變后獲得通知。訂閱者也稱為觀察者,而補觀察的對象稱為發(fā)布者或主題。當(dāng)發(fā)生了一個重要的事件時,發(fā)布者將會通知(調(diào)用)所有訂閱者并且可能經(jīng)常以事件對象的形式傳遞消息。
假設(shè)有一個發(fā)布者paper,它每天出版報紙及月刊雜志。訂閱者joe將被通知任何時候所發(fā)生的新聞。
該paper對象需要一個subscribers屬性,該屬性是一個存儲所有訂閱者的數(shù)組。訂閱行為只是將其加入到這個數(shù)組中。當(dāng)一個事件發(fā)生時,paper將會循環(huán)遍歷訂閱者列表并通知它們。通知意味著調(diào)用訂閱者對象的某個方法。故當(dāng)用戶訂閱信息時,該訂閱者需要向paper的subscribe()提供它的其中一個方法。
paper也提供了unsubscribe()方法,該方法表示從訂閱者數(shù)組(即subscribers屬性)中刪除訂閱者。paper最后一個重要的方法是publish(),它會調(diào)用這些訂閱者的方法,總而言之,發(fā)布者對象paper需要具有以下這些成員:
- ①subscribers 一個數(shù)組
- ②subscribe() 將訂閱者添加到subscribers數(shù)組中
- ③unsubscribe() 從subscribers數(shù)組中刪除訂閱者
- ④publish() 循環(huán)遍歷subscribers數(shù)組中的每一個元素,并且調(diào)用他們注冊時所提供的方法
所有這三種方法都需要一個type參數(shù),因為發(fā)布者可能觸發(fā)多個事件(比如同時發(fā)布一本雜志和一份報紙)而用戶可能僅選擇訂閱其中一種,而不是另外一種。
由于這些成員對于任何發(fā)布者對象都是通用的,故將它們作為獨立對象的一個部分來實現(xiàn)是很有意義的。那樣我們可將其復(fù)制到任何對象中,并將任意給定對象變成一個發(fā)布者。
三.用jQuery做一個簡單的實現(xiàn)
對于DOM事件的訂閱和發(fā)布,用jQuery實現(xiàn)起來是非常簡單的,接下來我們就是用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; }
對于上面這個實現(xiàn)來說,下面是一個User模型的最簡單的實現(xiàn)方法:
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; }
現(xiàn)在我們?nèi)绻胍獙ser模型屬性綁定到UI上,我們只需要將適合的數(shù)據(jù)特性綁定到對應(yīng)的HTML元素上。
// javascript var user = new User( 123 ); user.set( "name", "Wolfgang" ); // html <input type="number" data-bind-123="name" />
這樣輸入值會自動映射到user對象的name屬性,反之亦然。到此這個簡單實現(xiàn)就完成了。
四.Angular實現(xiàn)數(shù)據(jù)雙向綁定
Angular主要通過scopes實現(xiàn)數(shù)據(jù)雙向綁定。AngularJS的scopes包括以下四個主要部分:
digest循環(huán)以及dirty-checking,包括watch,digest,和$apply。
Scope繼承 - 這項機(jī)制使得我們可以創(chuàng)建scope繼承來分享數(shù)據(jù)和事件。
對集合 – 數(shù)組和對象 – 的有效dirty-checking。
事件系統(tǒng) - on,emit,以及$broadcast。
我們主要講解第一條Angular數(shù)據(jù)綁定是怎么實現(xiàn)的。
1.digest循環(huán)以及dirty-checking,包括watch,digest,和$apply
①瀏覽器事件循環(huán)和Angular.js擴(kuò)展
我們的瀏覽器一直在等待事件,比如用戶交互。假如你點擊一個按鈕或者在輸入框里輸入東西,事件的回調(diào)函數(shù)就會在javascript解釋器里執(zhí)行,然后你就可以做任何DOM操作,等回調(diào)函數(shù)執(zhí)行完畢時,瀏覽器就會相應(yīng)地對DOM做出變化。 Angular拓展了這個事件循環(huán),生成一個有時成為angular context的執(zhí)行環(huán)境(這是個重要的概念)。
②watch隊列(watch list)
每次你綁定一些東西到你的UI上時你就會往$watch隊列里插入一條$watch。想象一下$watch就是那個可以檢測它監(jiān)視的model里時候有變化的東西。
當(dāng)我們的模版加載完畢時,也就是在linking階段(Angular分為compile階段和linking階段---譯者注),Angular解釋器會尋找每個directive,然后生成每個需要的$watch。
③$digest循環(huán)
還記得我前面提到的擴(kuò)展的事件循環(huán)嗎?當(dāng)瀏覽器接收到可以被angular context處理的事件時,digest循環(huán)就會觸發(fā)。這個循環(huán)是由兩個更小的循環(huán)組合起來的。一個處理evalAsync隊列,另一個處理watch隊列。 這個是處理什么的呢?digest將會遍歷我們的watch,然后詢問它是否有屬性和值的變化,直$watch隊列都檢查過。
這就是所謂的dirty-checking。既然所有的$watch都檢查完了,那就要問了:有沒有$watch更新過?如果有至少一個更新過,這個循環(huán)就會再次觸發(fā),直到所有的$watch都沒有變化。這樣就能夠保證每個model都已經(jīng)不會再變化。記住如果循環(huán)超過10次的話,它將會拋出一個異常,防止無限循環(huán)。 當(dāng)$digest循環(huán)結(jié)束時,DOM相應(yīng)地變化。
例如:controllers.js
app.controller('MainCtrl', function() { $scope.name = "Foo"; $scope.changeFoo = function() { $scope.name = "Bar"; } });
{{ name }} <button ng-click="changeFoo()">Change the name</button>
這里我們有一個$watch因為ng-click不生成$watch(函數(shù)是不會變的)。
- 我們按下按鈕
- 瀏覽器接收到一個事件,進(jìn)入angular context(后面會解釋為什么)。
- $digest循環(huán)開始執(zhí)行,查詢每個$watch是否變化。
- 由于監(jiān)視$scope.name的$watch報告了變化,它會強制再執(zhí)行一次$digest循環(huán)。
- 新的$digest循環(huán)沒有檢測到變化。
- 瀏覽器拿回控制權(quán),更新與$scope.name新值相應(yīng)部分的DOM。
這里很重要的(也是許多人的很蛋疼的地方)是每一個進(jìn)入angular context的事件都會執(zhí)行一個$digest循環(huán),也就是說每次我們輸入一個字母循環(huán)都會檢查整個頁面的所有$watch。
④通過$apply來進(jìn)入angular context
誰決定什么事件進(jìn)入angular context,而哪些又不進(jìn)入呢?$apply!
如果當(dāng)事件觸發(fā)時,你調(diào)用apply,它會進(jìn)入angularcontext,如果沒有調(diào)用就不會進(jìn)入?,F(xiàn)在你可能會問:剛才的例子里我也沒有調(diào)用apply,為什么?Angular為你做了!因此你點擊帶有ng-click的元素時,時間就會被封裝到一個apply調(diào)用。如果你有一個ng−model="foo"的輸入框,然后你敲一個f,事件就會這樣調(diào)用apply("foo = 'f';")。
Angular什么時候不會自動為我們apply呢?這是Angular新手共同的痛處。為什么我的jQuery不會更新我綁定的東西呢?因為jQuery沒有調(diào)用apply,事件沒有進(jìn)入angular context,$digest循環(huán)永遠(yuǎn)沒有執(zhí)行。
2.具體實現(xiàn)
AngularJS的scopes就是一般的JavaScript對象,在它上面你可以綁定你喜歡的屬性和其他對象,然而,它們同時也被添加了一些功能用于觀察數(shù)據(jù)結(jié)構(gòu)上的變化。這些觀察的功能都由dirty-checking來實現(xiàn)并且都在一個digest循環(huán)中被執(zhí)行。
①Scope 對象
創(chuàng)建一個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); }); });
這個測試用來創(chuàng)建一個Scope,并在它上面賦一個任意值。我們可以輕松的讓這個測試通過:創(chuàng)建src/scope.js文件然后在其中添加以下內(nèi)容:
src/scope.js
------ /* jshint globalstrict: true */ 'use strict'; function Scope() { }
在這個測試中,我們將一個屬性(aProperty)賦值給了這個scope。這正是Scope上的屬性運行的方式。它們就是正常的JavaScript屬性,并沒有什么特別之處。這里你完全不需要去調(diào)用一個特別的setter,也不需要對你賦值的類型進(jìn)行什么限制。真正的魔法在于兩個特別的函數(shù):watch和digest。我們現(xiàn)在就來看看這兩個函數(shù)。
②監(jiān)視對象屬性:watch和digest
watch和digest是同一個硬幣的兩面。它們二者同時形成了$digest循環(huán)的核心:對數(shù)據(jù)的變化做出反應(yīng)。
為了實現(xiàn)這一塊功能,我們首先來定義一個測試文件并斷言你可以使用watch來注冊一個監(jiān)視器,并且當(dāng)有人調(diào)用了digest的時候監(jiān)視器的監(jiān)聽函數(shù)會被調(diào)用。
在scope_spec.js文件中添加一個嵌套的describe塊。并創(chuàng)建一個beforeEach函數(shù)來初始化這個scope,以便我們可以在進(jìn)行每個測試時重復(fù)它:
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(); }); }); });
在上面的這個測試中我們調(diào)用了watch來在這個scope上注冊一個監(jiān)視器。我們現(xiàn)在對于監(jiān)視函數(shù)本身并沒有什么興趣,因此我們隨便提供了一個函數(shù)來返回一個常數(shù)值。作為監(jiān)聽函數(shù),我們提供了一個JasmineSpy。接著我們調(diào)用了digest并檢查這個監(jiān)聽器是否真正被調(diào)用。
首先,這個Scope需要有一些地方去存儲所有被注冊的監(jiān)視器。我們現(xiàn)在就在Scope構(gòu)造函數(shù)中添加一個數(shù)組存儲它們:
src/scope.js
----- function Scope(){ this.$$watchers = []; }
上面代碼中的$$前綴在AngularJS框架中被認(rèn)為是私有變量,它們不應(yīng)該在應(yīng)用的外部被調(diào)用。
現(xiàn)在我們可以來定義watch函數(shù)了。它接收兩個函數(shù)作為參數(shù),并且將它們儲存在$watchers數(shù)組中。我們想要每一個Scope對象都擁有這個函數(shù),因此我們將它添加到Scope的原型中:
src/scope.js ----- Scope.prototype.$watch = function(watchFn, listenerFn) { var watcher = { watchFn: watchFn, listenerFn: listenerFn }; this.$$watchers.unshift(watcher); };
最后我們應(yīng)該有一個digest函數(shù)?,F(xiàn)在,我們來定義一個digest函數(shù)的簡化版本,它僅僅只是會迭代所有的注冊監(jiān)視器并調(diào)用它們的監(jiān)聽函數(shù):
digest能夠持續(xù)的迭代所有監(jiān)視函數(shù),直到被監(jiān)視的值停止變化。多做幾次digest是我們能夠獲得運用于監(jiān)視器并依賴于其他監(jiān)視器的變化。
首先,我們新建名為$$digestOnce,并且調(diào)整它以便它能夠在所有監(jiān)視器上運行一遍,然后返回一個布爾值來說明有沒有任何變化:
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以便它能夠運行“外循環(huán)”,在變化發(fā)生時調(diào)用$digestOnce:
src/scope.js
----- Scope.prototype.$digest = function(){ var dirty; do { dirty = this.$$digestOnce(); } while (dirty); };
以上就是Angular數(shù)據(jù)雙向綁定的相關(guān)介紹,希望對大家的學(xué)習(xí)有所幫助。
相關(guān)文章
Angular.js中$resource高大上的數(shù)據(jù)交互詳解
這篇文章主要給大家介紹了關(guān)于Angular.js中$resource高大上的數(shù)據(jù)交互的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家學(xué)習(xí)或者使用angular.js具有一定的參考學(xué)習(xí)價值,需要的朋友們下面跟著小編來一起看看吧。2017-07-07Angular.JS中select下拉框設(shè)置value的方法
select 是 AngularJS 預(yù)設(shè)的一組directive。下面這篇文章主要給大家介紹了關(guān)于Angular.JS中select下拉框設(shè)置value的方法,文中介紹的非常詳細(xì),對大家具有一定的參考學(xué)習(xí)價值,需要的朋友們下面來一起看看吧。2017-06-06詳解Angular路由 ng-route和ui-router的區(qū)別
這篇文章主要介紹了詳解Angular路由 ng-route和ui-router的區(qū)別,分別介紹了兩種路由的用法和區(qū)別,有興趣的可以了解一下2017-05-05