實例剖析AngularJS框架中數(shù)據(jù)的雙向綁定運用
數(shù)據(jù)綁定
通過把一個文本輸入框綁定到person.name屬性上,就能把我們的應用變得更有趣一點。這一步建立起了文本輸入框跟頁面的雙向綁定。
在這個語境里“雙向”意味著如果view改變了屬性值,model就會“看到”這個改變,而如果model改變了屬性值,view也同樣會“看到”這個改變。Angular.js 為你自動搭建好了這個機制。如果你好奇這具體是怎么實現(xiàn)的,請看我們之后推出的一篇文章,其中深入討論了digest_loop 的運作。
要建立這個綁定,我們在文本輸入框上使用ng-model 指令屬性,像這樣:
<div ng-controller="MyController"> <input type="text" ng-model="person.name" placeholder="Enter your name" /> <h5>Hello {{ person.name }}</h5> </div>
現(xiàn)在我們建立好了一個數(shù)據(jù)綁定(沒錯,就這么容易),來看看view怎么改變model吧:
試試看:
當你在文本框里輸入時,下面的名字也自動隨之改變,這就展現(xiàn)了我們數(shù)據(jù)綁定的一個方向:從view到model。
我們也可以在我們的(客戶端)后臺改變model,看這個改變自動在前端體現(xiàn)出來。要展示這一過程,讓我們在 MyController 的model里寫一個計時器函數(shù), 更新 $scope 上的一個數(shù)據(jù)。下面的代碼里,我們就來創(chuàng)建這個計時器函數(shù),它會在每秒計時(像鐘表那樣),并更新 $scope 上的clock變量數(shù)據(jù):
app.controller('MyController', function($scope) { $scope.person = { name: "Ari Lerner" }; var updateClock = function() { $scope.clock = new Date(); }; var timer = setInterval(function() { $scope.$apply(updateClock); }, 1000); updateClock(); });
可以看到,當我們改變model中clock變量的數(shù)據(jù),view會自動更新來反映此變化。用大括號我們就可以很簡單地讓clock變量的值顯示在view里:
<div ng-controller="MyController"> <h5>{{ clock }}</h5> </div>
互動
前面我們把數(shù)據(jù)綁定在了文本輸入框上。請注意, 數(shù)據(jù)綁定并非只限于數(shù)據(jù),我們還可以利用綁定調(diào)用 $scope 中的函數(shù)(這一點之前已經(jīng)提到過)。
對按鈕、鏈接或任何其他的DOM元素,我們都可以用另一個指令屬性來實現(xiàn)綁定:ng-click 。這個 ng-click 指令將DOM元素的鼠標點擊事件(即 mousedown 瀏覽器事件)綁定到一個方法上,當瀏覽器在該DOM元素上鼠標觸發(fā)點擊事件時,此被綁定的方法就被調(diào)用。跟上一個例子相似,這個綁定的代碼如下:
<div ng-controller="DemoController"> <h4>The simplest adding machine ever</h4> <button ng-click="add(1)" class="button">Add</button> <button ng-click="subtract(1)" class="button">Subtract</button> <h4>Current count: {{ counter }}</h4> </div>
不論是按鈕還是鏈接都會被綁定到包含它們的DOM元素的controller所有的 $scope 對象上,當它們被鼠標點擊,Angular就會調(diào)用相應的方法。注意當我們告訴Angular要調(diào)用什么方法時,我們將方法名寫進帶引號的字符串里。
app.controller('DemoController', function($scope) { $scope.counter = 0; $scope.add = function(amount) { $scope.counter += amount; }; $scope.subtract = function(amount) { $scope.counter -= amount; }; });
請看:
$scope.$watch
$scope.$watch( watchExp, listener, objectEquality );
為了監(jiān)視一個變量的變化,你可以使用$scope.$watch函數(shù)。這個函數(shù)有三個參數(shù),它指明了”要觀察什么”(watchExp),”在變化時要發(fā)生什么”(listener),以及你要監(jiān)視的是一個變量還是一個對象。當我們在檢查一個參數(shù)時,我們可以忽略第三個參數(shù)。例如下面的例子:
$scope.name = 'Ryan'; $scope.$watch( function( ) { return $scope.name; }, function( newValue, oldValue ) { console.log('$scope.name was updated!'); } );
AngularJS將會在$scope中注冊你的監(jiān)視函數(shù)。你可以在控制臺中輸出$scope來查看$scope中的注冊項目。
你可以在控制臺中看到$scope.name已經(jīng)發(fā)生了變化 – 這是因為$scope.name之前的值似乎undefined而現(xiàn)在我們將它賦值為Ryan!
對于$wach的第一個參數(shù),你也可以使用一個字符串。這和提供一個函數(shù)完全一樣。在AngularJS的源代碼中可以看到,如果你使用了一個字符串,將會運行下面的代碼:
if (typeof watchExp == 'string' && get.constant) { var originalFn = watcher.fn; watcher.fn = function(newVal, oldVal, scope) { originalFn.call(this, newVal, oldVal, scope); arrayRemove(array, watcher); }; }
這將會把我們的watchExp設置為一個函數(shù),它也自動返回作用域中我們已經(jīng)制定了名字的變量。
$$watchers
$scope中的$$watchers變量保存著我們定義的所有的監(jiān)視器。如果你在控制臺中查看$$watchers,你會發(fā)現(xiàn)它是一個對象數(shù)組。
$$watchers = [ { eq: false, // 表明我們是否需要檢查對象級別的相等 fn: function( newValue, oldValue ) {}, // 這是我們提供的監(jiān)聽器函數(shù) last: 'Ryan', // 變量的最新值 exp: function(){}, // 我們提供的watchExp函數(shù) get: function(){} // Angular's編譯后的watchExp函數(shù) } ];
$watch函數(shù)將會返回一個deregisterWatch函數(shù)。這意味著如果我們使用$scope.$watch對一個變量進行監(jiān)視,我們也可以在以后通過調(diào)用某個函數(shù)來停止監(jiān)視。
$scope.$apply
當一個控制器/指令/等等東西在AngularJS中運行時,AngularJS內(nèi)部會運行一個叫做$scope.$apply的函數(shù)。這個$apply函數(shù)會接收一個函數(shù)作為參數(shù)并運行它,在這之后才會在rootScope上運行$digest函數(shù)。
AngularJS的$apply函數(shù)代碼如下所示:
$apply: function(expr) { try { beginPhase('$apply'); return this.$eval(expr); } catch (e) { $exceptionHandler(e); } finally { clearPhase(); try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }
上面代碼中的expr參數(shù)就是你在調(diào)用$scope.$apply()時傳遞的參數(shù) – 但是大多數(shù)時候你可能都不會去使用$apply這個函數(shù),要用的時候記得給它傳遞一個參數(shù)。
下面我們來看看ng-keydown是怎么來使用$scope.$apply的。為了注冊這個指令,AngularJS會使用下面的代碼。
var ngEventDirectives = {}; forEach( 'click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(' '), function(name) { var directiveName = directiveNormalize('ng-' + name); ngEventDirectives[directiveName] = ['$parse', function($parse) { return { compile: function($element, attr) { var fn = $parse(attr[directiveName]); return function ngEventHandler(scope, element) { element.on(lowercase(name), function(event) { scope.$apply(function() { fn(scope, {$event:event}); }); }); }; } }; }]; } );
上面的代碼做的事情是循環(huán)了不同的類型的事件,這些事件在之后可能會被觸發(fā)并創(chuàng)建一個叫做ng-[某個事件]的新指令。在指令的compile函數(shù)中,它在元素上注冊了一個事件處理器,它和指令的名字一一對應。當事件被出發(fā)時,AngularJS就會運行scope.$apply函數(shù),并讓它運行一個函數(shù)。
只是單向數(shù)據(jù)綁定嗎?
上面所說的ng-keydown只能夠改變和元素值相關(guān)聯(lián)的$scope中的值 – 這只是單項數(shù)據(jù)綁定。這也是這個指令叫做ng-keydown的原因,只有在keydown事件被觸發(fā)時,能夠給與我們一個新值。
但是我們想要的是雙向數(shù)據(jù)綁定!
我們現(xiàn)在來看一看ng-model。當你在使用ng-model時,你可以使用雙向數(shù)據(jù)綁定 – 這正是我們想要的。AngularJS使用$scope.$watch(視圖到模型)以及$scope.$apply(模型到視圖)來實現(xiàn)這個功能。
ng-model會把事件處理指令(例如keydown)綁定到我們運用的輸入元素上 – 這就是$scope.$apply被調(diào)用的地方!而$scope.$watch是在指令的控制器中被調(diào)用的。你可以在下面代碼中看到這一點:
$scope.$watch(function ngModelWatch() { var value = ngModelGet($scope); //如果作用域模型值和ngModel值沒有同步 if (ctrl.$modelValue !== value) { var formatters = ctrl.$formatters, idx = formatters.length; ctrl.$modelValue = value; while(idx--) { value = formatters[idx](value); } if (ctrl.$viewValue !== value) { ctrl.$viewValue = value; ctrl.$render(); } } return value; });
如果你在調(diào)用$scope.$watch時只為它傳遞了一個參數(shù),無論作用域中的什么東西發(fā)生了變化,這個函數(shù)都會被調(diào)用。在ng-model中,這個函數(shù)被用來檢查模型和視圖有沒有同步,如果沒有同步,它將會使用新值來更新模型數(shù)據(jù)。這個函數(shù)會返回一個新值,當它在$digest函數(shù)中運行時,我們就會知道這個值是什么!
為什么我們的監(jiān)聽器沒有被觸發(fā)?
如果我們在$scope.$watch的監(jiān)聽器函數(shù)中停止這個監(jiān)聽,即使我們更新了$scope.name,該監(jiān)聽器也不會被觸發(fā)。
正如前面所提到的,AngularJS將會在每一個指令的控制器函數(shù)中運行$scope.$apply。如果我們查看$scope.$apply函數(shù)的代碼,我們會發(fā)現(xiàn)它只會在控制器函數(shù)已經(jīng)開始被調(diào)用之后才會運行$digest函數(shù) – 這意味著如果我們馬上停止監(jiān)聽,$scope.$watch函數(shù)甚至都不會被調(diào)用!但是它究竟是怎樣運行的呢?
$digest函數(shù)將會在$rootScope中被$scope.$apply所調(diào)用。它將會在$rootScope中運行digest循環(huán),然后向下遍歷每一個作用域并在每個作用域上運行循環(huán)。在簡單的情形中,digest循環(huán)將會觸發(fā)所有位于$$watchers變量中的所有watchExp函數(shù),將它們和最新的值進行對比,如果值不相同,就會觸發(fā)監(jiān)聽器。
當digest循環(huán)運行時,它將會遍歷所有的監(jiān)聽器然后再次循環(huán),只要這次循環(huán)發(fā)現(xiàn)了”臟值”,循環(huán)就會繼續(xù)下去。如果watchExp的值和最新的值不相同,那么這次循環(huán)就會被認為發(fā)現(xiàn)了臟值。理想情況下它會運行一次,如果它運行超10次,你會看到一個錯誤。
因此當$scope.$apply運行的時候,$digest也會運行,它將會循環(huán)遍歷$$watchers,只要發(fā)現(xiàn)watchExp和最新的值不相等,變化觸發(fā)事件監(jiān)聽器。在AngularJS中,只要一個模型的值可能發(fā)生變化,$scope.$apply就會運行。這就是為什么當你在AngularJS之外更新$scope時,例如在一個setTimeout函數(shù)中,你需要手動去運行$scope.$apply():這能夠讓AngularJS意識到它的作用域發(fā)生了變化。
創(chuàng)建自己的臟值檢查
到此為止,我們已經(jīng)可以來創(chuàng)建一個小巧的,簡化版本的臟值檢查了。當然,相比較之下,AngularJS中實現(xiàn)的臟值檢查要更加先進一些,它提供瘋了異步隊列以及其他一些高級功能。
設置Scope
Scope僅僅只是一個函數(shù),它其中包含任何我們想要存儲的對象。我們可以擴展這個函數(shù)的原型對象來復制$digest和$watch。我們不需要$apply方法,因為我們不需要在作用域的上下文中執(zhí)行任何函數(shù) – 我們只需要簡單的使用$digest。我們的Scope的代碼如下所示:
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( ) { }; Scope.prototype.$digest = function( ) { };
我們的$watch函數(shù)需要接受兩個參數(shù),watchExp和listener。當$watch被調(diào)用時,我們需要將它們push進入到Scope的$$watcher數(shù)組中。
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { };
你可能已經(jīng)注意到了,如果沒有提供listener,我們會將listener設置為一個空函數(shù) – 這樣一來我們可以$watch所有的變量。
接下來我們將會創(chuàng)建$digest。我們需要來檢查舊值是否等于新的值,如果二者不相等,監(jiān)聽器就會被觸發(fā)。我們會一直循環(huán)這個過程,直到二者相等。這就是”臟值”的來源 – 臟值意味著新的值和舊的值不相等!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); };
接下來,我們將創(chuàng)建一個作用域的實例。我們將這個實例賦值給$scope。我們接著會注冊一個監(jiān)聽函數(shù),在更新$scope之后運行$digest!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); }; var $scope = new Scope(); $scope.name = 'Ryan'; $scope.$watch(function(){ return $scope.name; }, function( newValue, oldValue ) { console.log(newValue, oldValue); } ); $scope.$digest();
成功了!我們現(xiàn)在已經(jīng)實現(xiàn)了臟值檢查(雖然這是最簡單的形式)!上述代碼將會在控制臺中輸出下面的內(nèi)容:
Ryan undefined
這正是我們想要的結(jié)果 – $scope.name之前的值是undefined,而現(xiàn)在的值是Ryan。
現(xiàn)在我們把$digest函數(shù)綁定到一個input元素的keyup事件上。這就意味著我們不需要自己去調(diào)用$digest。這也意味著我們現(xiàn)在可以實現(xiàn)雙向數(shù)據(jù)綁定!
var Scope = function( ) { this.$$watchers = []; }; Scope.prototype.$watch = function( watchExp, listener ) { this.$$watchers.push( { watchExp: watchExp, listener: listener || function() {} } ); }; Scope.prototype.$digest = function( ) { var dirty; do { dirty = false; for( var i = 0; i < this.$$watchers.length; i++ ) { var newValue = this.$$watchers[i].watchExp(), oldValue = this.$$watchers[i].last; if( oldValue !== newValue ) { this.$$watchers[i].listener(newValue, oldValue); dirty = true; this.$$watchers[i].last = newValue; } } } while(dirty); }; var $scope = new Scope(); $scope.name = 'Ryan'; var element = document.querySelectorAll('input'); element[0].onkeyup = function() { $scope.name = element[0].value; $scope.$digest(); }; $scope.$watch(function(){ return $scope.name; }, function( newValue, oldValue ) { console.log('Input value updated - it is now ' + newValue); element[0].value = $scope.name; } ); var updateScopeValue = function updateScopeValue( ) { $scope.name = 'Bob'; $scope.$digest(); };
使用上面的代碼,無論何時我們改變了input的值,$scope中的name屬性都會相應的發(fā)生變化。這就是隱藏在AngularJS神秘外衣之下數(shù)據(jù)雙向綁定的秘密!
- javascript實現(xiàn)數(shù)據(jù)雙向綁定的三種方式小結(jié)
- Vue.js每天必學之數(shù)據(jù)雙向綁定
- 輕松實現(xiàn)javascript數(shù)據(jù)雙向綁定
- 深入學習AngularJS中數(shù)據(jù)的雙向綁定機制
- 淺談AngularJs 雙向綁定原理(數(shù)據(jù)綁定機制)
- Vue.js第一天學習筆記(數(shù)據(jù)的雙向綁定、常用指令)
- 深入理解Angularjs向指令傳遞數(shù)據(jù)雙向綁定機制
- AngularJS學習筆記(三)數(shù)據(jù)雙向綁定的簡單實例
- JS原生數(shù)據(jù)雙向綁定實現(xiàn)代碼
- js實現(xiàn)數(shù)據(jù)雙向綁定(訪問器監(jiān)聽)
相關(guān)文章
基于?angular?material?theming?機制修改?mat-toolbar?的背景色(示例詳解
最近在學習?angular,記錄一下昨天的進展,解決的問題是通過?theme?的配置修改?mat-toolbar?的背景色,避免對色彩的硬編碼,這篇文章主要介紹了基于?angular?material?theming?機制修改?mat-toolbar?的背景色,需要的朋友可以參考下2022-10-10AngularJS2中一種button切換效果的實現(xiàn)方法(二)
這篇文章主要介紹了AngularJS2中一種button切換效果的實現(xiàn)方法(二),非常不錯,具有參考借鑒價值,需要的朋友可以參考下2017-03-03AngularJS控制器controller給模型數(shù)據(jù)賦初始值的方法
這篇文章主要介紹了AngularJS控制器controller給模型數(shù)據(jù)賦初始值的方法,涉及AngularJS控制器controller簡單賦值操作實現(xiàn)技巧,需要的朋友可以參考下2017-01-01Angular指令封裝jQuery日期時間插件datetimepicker實現(xiàn)雙向綁定示例
這篇文章主要介紹了Angular指令封裝jQuery日期時間插件datetimepicker實現(xiàn)雙向綁定示例,具有一定的參考價值,有興趣的可以了解一下。2017-01-01