執行個體剖析AngularJS架構中資料的雙向繫結運用_AngularJS

來源:互聯網
上載者:User

資料繫結

通過把一個文本輸入框綁定到person.name屬性上,就能把我們的應用變得更有趣一點。這一步建立起了文本輸入框跟頁面的雙向繫結。

在這個語境裡“雙向”意味著如果view改變了屬性值,model就會“看到”這個改變,而如果model改變了屬性值,view也同樣會“看到”這個改變。Angular.js 為你自動搭建好了這個機制。如果你好奇這具體是怎麼實現的,請看我們之後推出的一篇文章,其中深入討論了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>

現在我們建立好了一個資料繫結(沒錯,就這麼容易),來看看view怎麼改變model吧:

試試看:

當你在文字框裡輸入時,下面的名字也自動隨之改變,這就展現了我們資料繫結的一個方向:從view到model。

我們也可以在我們的(用戶端)後台改變model,看這個改變自動在前端體現出來。要展示這一過程,讓我們在  MyController 的model裡寫一個計時器函數, 更新 $scope 上的一個資料。下面的代碼裡,我們就來建立這個計時器函數,它會在每秒計時(像鐘錶那樣),並更新 $scope 上的clock變數資料:

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變數的資料,view會自動更新來反映此變化。用大括弧我們就可以很簡單地讓clock變數的值顯示在view裡:

<div ng-controller="MyController"> <h5>{{ clock }}</h5></div>


互動

前面我們把資料繫結在了文本輸入框上。請注意, 資料繫結並非只限於資料,我們還可以利用綁定調用 $scope 中的函數(這一點之前已經提到過)。

對按鈕、連結或任何其他的DOM元素,我們都可以用另一個指令屬性來實現綁定:ng-click 。這個 ng-click 指令將DOM元素的滑鼠點擊事件(即 mousedown 瀏覽器事件)綁定到一個方法上,當瀏覽器在該DOM元素上滑鼠觸發點擊事件時,此被綁定的方法就被調用。跟上一個例子相似,這個綁定的代碼如下:

<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就會調用相應的方法。注意當我們告訴Angular要調用什麼方法時,我們將方法名寫進帶引號的字串裡。

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 );

為了監視一個變數的變化,你可以使用$scope.$watch函數。這個函數有三個參數,它指明了”要觀察什麼”(watchExp),”在變化時要發生什麼”(listener),以及你要監視的是一個變數還是一個對象。當我們在檢查一個參數時,我們可以忽略第三個參數。例如下面的例子:

$scope.name = 'Ryan';$scope.$watch( function( ) {  return $scope.name;}, function( newValue, oldValue ) {  console.log('$scope.name was updated!');} );

AngularJS將會在$scope中註冊你的監視函數。你可以在控制台中輸出$scope來查看$scope中的註冊項目。

你可以在控制台中看到$scope.name已經發生了變化 – 這是因為$scope.name之前的值似乎undefined而現在我們將它賦值為Ryan!

對於$wach的第一個參數,你也可以使用一個字串。這和提供一個函數完全一樣。在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設定為一個函數,它也自動返回範圍中我們已經制定了名字的變數。

$$watchers
$scope中的$$watchers變數儲存著我們定義的所有的監視器。如果你在控制台中查看$$watchers,你會發現它是一個對象數組。

$$watchers = [  {    eq: false, // 表明我們是否需要檢查對象層級的相等    fn: function( newValue, oldValue ) {}, // 這是我們提供的監聽器函數    last: 'Ryan', // 變數的最新值    exp: function(){}, // 我們提供的watchExp函數    get: function(){} // Angular's編譯後的watchExp函數  }];

$watch函數將會返回一個deregisterWatch函數。這意味著如果我們使用$scope.$watch對一個變數進行監視,我們也可以在以後通過調用某個函數來停止監視。

$scope.$apply
當一個控制器/指令/等等東西在AngularJS中運行時,AngularJS內部會運行一個叫做$scope.$apply的函數。這個$apply函數會接收一個函數作為參數並運行它,在這之後才會在rootScope上運行$digest函數。

AngularJS的$apply函數代碼如下所示:

$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參數就是你在調用$scope.$apply()時傳遞的參數 – 但是大多數時候你可能都不會去使用$apply這個函數,要用的時候記得給它傳遞一個參數。

下面我們來看看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});       });      });     };    }   };  }]; });

上面的代碼做的事情是迴圈了不同的類型的事件,這些事件在之後可能會被觸發並建立一個叫做ng-[某個事件]的新指令。在指令的compile函數中,它在元素上註冊了一個事件處理器,它和指令的名字一一對應。當事件被出發時,AngularJS就會運行scope.$apply函數,並讓它運行一個函數。

只是單向資料繫結嗎?
上面所說的ng-keydown只能夠改變和元素值相關聯的$scope中的值 – 這隻是單項資料繫結。這也是這個指令叫做ng-keydown的原因,只有在keydown事件被觸發時,能夠給與我們一個新值。

但是我們想要的是雙向資料繫結!
我們現在來看一看ng-model。當你在使用ng-model時,你可以使用雙向資料繫結 – 這正是我們想要的。AngularJS使用$scope.$watch(視圖到模型)以及$scope.$apply(模型到視圖)來實現這個功能。

ng-model會把事件處理指示(例如keydown)綁定到我們運用的輸入元素上 – 這就是$scope.$apply被調用的地方!而$scope.$watch是在指令的控制器中被調用的。你可以在下面代碼中看到這一點:

$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;});

如果你在調用$scope.$watch時只為它傳遞了一個參數,無論範圍中的什麼東西發生了變化,這個函數都會被調用。在ng-model中,這個函數被用來檢查模型和視圖有沒有同步,如果沒有同步,它將會使用新值來更新模型資料。這個函數會返回一個新值,當它在$digest函數中運行時,我們就會知道這個值是什麼!

為什麼我們的監聽器沒有被觸發?
如果我們在$scope.$watch的監聽器函數中停止這個監聽,即使我們更新了$scope.name,該監聽器也不會被觸發。

正如前面所提到的,AngularJS將會在每一個指令的控制器函數中運行$scope.$apply。如果我們查看$scope.$apply函數的代碼,我們會發現它只會在控制器函數已經開始被調用之後才會運行$digest函數 – 這意味著如果我們馬上停止監聽,$scope.$watch函數甚至都不會被調用!但是它究竟是怎樣啟動並執行呢?

$digest函數將會在$rootScope中被$scope.$apply所調用。它將會在$rootScope中運行digest迴圈,然後向下遍曆每一個範圍並在每個範圍上運行迴圈。在簡單的情形中,digest迴圈將會觸發所有位於$$watchers變數中的所有watchExp函數,將它們和最新的值進行對比,如果值不相同,就會觸發監聽器。

當digest迴圈運行時,它將會遍曆所有的監聽器然後再次迴圈,只要這次迴圈發現了”髒值”,迴圈就會繼續下去。如果watchExp的值和最新的值不相同,那麼這次迴圈就會被認為發現了髒值。理想情況下它會運行一次,如果它運行超10次,你會看到一個錯誤。

因此當$scope.$apply啟動並執行時候,$digest也會運行,它將會迴圈遍曆$$watchers,只要發現watchExp和最新的值不相等,變化觸發事件監聽器。在AngularJS中,只要一個模型的值可能發生變化,$scope.$apply就會運行。這就是為什麼當你在AngularJS之外更新$scope時,例如在一個setTimeout函數中,你需要手動去運行$scope.$apply():這能夠讓AngularJS意識到它的範圍發生了變化。

建立自己的髒值檢查
到此為止,我們已經可以來建立一個小巧的,簡化版本的髒值檢查了。當然,相比較之下,AngularJS中實現的髒值檢查要更加先進一些,它提供瘋了非同步隊列以及其他一些進階功能。

設定Scope
Scope僅僅只是一個函數,它其中包含任何我們想要儲存的對象。我們可以擴充這個函數的原型對象來複製$digest和$watch。我們不需要$apply方法,因為我們不需要在範圍的上下文中執行任何函數 – 我們只需要簡單的使用$digest。我們的Scope的代碼如下所示:

var Scope = function( ) {  this.$$watchers = [];  };Scope.prototype.$watch = function( ) {};Scope.prototype.$digest = function( ) {};

我們的$watch函數需要接受兩個參數,watchExp和listener。當$watch被調用時,我們需要將它們push進入到Scope的$$watcher數組中。

var Scope = function( ) {  this.$$watchers = [];  };Scope.prototype.$watch = function( watchExp, listener ) {  this.$$watchers.push( {    watchExp: watchExp,    listener: listener || function() {}  } );};Scope.prototype.$digest = function( ) {};

你可能已經注意到了,如果沒有提供listener,我們會將listener設定為一個空函數 – 這樣一來我們可以$watch所有的變數。

接下來我們將會建立$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);};

接下來,我們將建立一個範圍的執行個體。我們將這個執行個體賦值給$scope。我們接著會註冊一個監聽函數,在更新$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();

成功了!我們現在已經實現了髒值檢查(雖然這是最簡單的形式)!上述代碼將會在控制台中輸出下面的內容:

Ryan undefined

這正是我們想要的結果 – $scope.name之前的值是undefined,而現在的值是Ryan。

現在我們把$digest函數綁定到一個input元素的keyup事件上。這就意味著我們不需要自己去調用$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';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屬性都會相應的發生變化。這就是隱藏在AngularJS神秘外衣之下資料雙向繫結的秘密!

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.