本篇詳細介紹:1.angular時如何經過髒檢查來實現對$scope對象上變量的雙向綁定的。2.實現angular雙向綁定的三個重要方法:$digest(),$apply(),$watch().node
angular不像Ember.js,經過動態設置setter函數和getter函數來實現雙向綁定,髒檢查容許angular監聽可能存在可能不存在的變量。express
$scope.$watch語法糖:$scope.$watch(watchExp,Listener,objectEquality);數組
監聽一個變量什麼時候變化,須要調用$scope.$watch函數,這個函數接受三個參數:須要檢測的值或者表達式(watchExp),監聽函數,值變化時執行(Listener匿名函數),是否開啓值檢測,爲 true時會檢測對象或者數組的內部變動(即選擇以===的方式比較仍是angular.equals的方式)。舉個例子:瀏覽器
1 $scope.name = 'Ryan'; 2 3 $scope.$watch( function( ) { 4 return $scope.name; 5 }, function( newValue, oldValue ) { 6 console.log('$scope.name was updated!'); 7 } );
angular會在$scope對象上註冊你的監聽函數Listener,你能夠注意到會有日誌輸出「$scope.name was updated!」,由於$scope.name由先前的undefined更新爲‘Ryan’。固然,watcher也能夠是一個字符串,效果和上面例子中的匿名函數同樣,在angular源碼中,app
1 if(typeof watchExp == 'string' &&get.constant){ 2 var originalFn = watcher.fn; 3 watcher.fn = function(newVal, oldVal, scope) { 4 originalFn.call(this, newVal, oldVal, scope); 5 arrayRemove(array, watcher); 6 }; 7 }
上面這段代碼將watchExp設置爲一個函數,這個函數會調用帶有給定變量名的listener函數。dom
下面舉個應用實例,以插值{{post.title}}爲例,當angular在compile編譯階段遇到這個語法元素時,內部處理邏輯以下:ide
walkers.expression = function( ast ){ var node = document.createTextNode(""); this.$watch(ast, function(newval){ dom.text(node, "" + (newval == null? "": "" + newval) ); }) return node; }
這段代碼很好理解,就是當遇到插值時,會新建一個textNode,並把值寫入到該nodeContent中.那麼angular怎麼判斷這個節點值改變或者說新增了一個節點?函數
這裏就不得不提到$digest函數。首先,經過$watch接口,會產生一個監聽隊列$$watchers。$scope對象下的的$$watchers對象下擁有你定義的全部的watchers。若是你進入到$$watchers內部,會發現它這樣的一個數組。oop
$$watchers = [ { eq: false, // whether or not we are checking for objectEquality 是否須要判斷對象級別的相等 fn: function( newValue, oldValue ) {}, // this is the listener function we've provided 這是咱們提供的監聽器函數 last: 'Ryan', // the last known value for the variable$nbsp;$nbsp;變量的最新值 exp: function(){}, // this is the watchExp function we provided$nbsp;$nbsp;咱們提供的watchExp函數 get: function(){} // Angular's compiled watchExp function angualr編譯過的watchExp函數 } ];
$watch函數會返回一個deregisterWatch function,這意味着若是咱們使用scope.$watch對一個變量進行監視,那麼也能夠經過調用deregisterWatch這個函數來中止監聽。post
我是萌萌嗒分割線
在angularJs中,當一個controller/directive/etc在運行時,angular內部會先運行$scope.$apply()函數,這個函數接受一個參數,參數爲一個函數fn,這個函數就是用來執行fn函數的,執行完fn後纔會在$rootScope做用域中運行$scope.$digest這個函數。angular源碼中時這樣描述$apply這個函數的。
$apply: function(expr) { try { beginPhase('$apply'); try { return this.$eval(expr); } finally { clearPhase(); } } catch (e) { $exceptionHandler(e); } finally { try { $rootScope.$digest(); } catch (e) { $exceptionHandler(e); throw e; } } }
上面的expr這個參數其實是一個函數,這個函數是你或者angular在調scope.$apply這個函數時傳入的。可是大多數時候你可能都不會去使用這個函數,用的時候記得給他傳入一個function參數。
ok,說了這麼多,讓咱們看看angular事怎麼使用$scope.$apply的,下面以ng-keydown這個指令來舉例,爲了註冊這個指令,且看源碼是如何申明的:
var ngDirectives = {}; forEach('click dblclick mousedown mouseup mouseover mouseout mousemove mouseenter mouseleave keydown keyup keypress submit focus blur copy cut paste'.split(','),function(){ 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}); }); }); }; } }; }]; });
上面這段代碼遍歷了各類不一樣的可能被觸發的event類型,並建立一個叫ng-[EventNameHere](中括號中爲事件名),在這個directive的的compile函數中,它在元素上註冊了一個事件處理器,事件和對應的directive名字一一對應,好比,cilck事件和ng-click指令對應。當click事件被觸發(或者說ng-click指令被觸發),angular會執行scope.$apply,執行$apply中的參數(參數爲function)。
上面的代碼只是改變了和元素(elment)相關聯的$scope中的值。這只是單向綁定。這也是這個指令叫作ng-keydown的緣由,只有在keydown事件被觸發時,可以給與咱們一個新值。不是說angular實現了雙向數據綁定嗎?!
下面看一看ng-model這個directive,當你在使用ng-model時,你可使用雙向數據綁定 – 這正是咱們想要的。AngularJS使用$scope.$watch(視圖到模型)以及$scope.$apply(模型到視圖)來實現這個功能。
ng-model會把事件處理指令(例如keydown)綁定到咱們運用的輸入元素上 – 這就是$scope.$apply被調用的地方!而$scope.$watch是在指令的控制器中被調用的。你能夠在下面代碼中看到這一點:
$scope.$watch(function ngModelWatch() { //獲取ngModelController中的$scope對象,即數據模型;
var value = ngModelGet($scope); //若是做用域模型值和ngModel值沒有同步;$modelValue爲模型綁定的值,value爲數據模型的真實值,$viewValue爲視圖中展現的值。ngModel.ngMOdelController.$gormatters屬性是爲了格式化或者轉化ngModel控制器中數據模型,$render函數在$modelValue和$viewValue不相等時,須要調用。 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函數中運行時,咱們就會知道這個值是什麼!
那麼,爲何有時候咱們的監聽器並無被觸發或者說不起做用?
正如前面所提到的,AngularJS將會在每個指令的控制器函數中運行$scope.$apply。若是咱們查看$scope.$apply函數的代碼,咱們會發現它只會在控制器函數已經開始被調用以後纔會運行$digest函數 – 這意味着若是咱們立刻中止監聽,$scope.$watch函數甚至都不會被調用!所以當$scope.$apply運行的時候,$digest也會運行,它將會循環遍歷$$watchers,只要發現watchExp和最新的值不相等,變化觸發事件監聽器。在AngularJS中,只要一個模型的值可能發生變化,$scope.$apply就會運行。這就是爲何當你在AngularJS以外更新$scope時,例如在一個setTimeout函數中,你須要手動去運行$scope.$apply():這可以讓AngularJS意識到它的做用域發生了變化。
可是digest過程到底是怎樣運行的呢?(下面仔細探索源碼中$digest函數執行流程,能夠不看。。。)
1.首先,標記dirty = false ;
2.遍歷當前做用域中的監聽對象(current.$$watchers),而且經過判斷當前監聽對象數組中值watch.get(current)和老值watch.last是否相等:若是不相等,將標記dirty設置成true,將上一個監聽對象lastDirtyWatch賦值爲當前監聽對象,而且將監聽對象的老值watch.last賦值爲新值,最後,調用watch對象綁定的Listener函數wantch.fn。
traverseScopesLoop: do { // "traverse the scopes" loop if ((watchers = current.$$watchers)) { // process our watches length = watchers.length; while (length--) { try { watch = watchers[length]; // Most common watches are on primitives, in which case we can short // circuit it with === operator, only when === fails do we use .equals if (watch) { if ((value = watch.get(current)) !== (last = watch.last) && !(watch.eq ? equals(value, last) : (typeof value === 'number' && typeof last === 'number' && isNaN(value) && isNaN(last)))) { dirty = true; lastDirtyWatch = watch; watch.last = watch.eq ? copy(value, null) : value; watch.fn(value, ((last === initWatchVal) ? value : last), current); if (ttl < 5) { logIdx = 4 - ttl; if (!watchLog[logIdx]) watchLog[logIdx] = []; watchLog[logIdx].push({ msg: isFunction(watch.exp) ? 'fn: ' + (watch.exp.name || watch.exp.toString()) : watch.exp, newVal: value, oldVal: last }); } } else if (watch === lastDirtyWatch) { // If the most recently dirty watcher is now clean, short circuit since the remaining watchers // have already been tested. dirty = false; break traverseScopesLoop; } } } catch (e) { $exceptionHandler(e); } } }
3.進入下一個watch的檢查,遍歷檢查一輪後,若是dirty===true
,咱們從新進入步驟1. 不然進入步驟4.
4.完成髒檢查。
最後,表達一下我的對這塊的見解。做爲初學的話,不須要去理解他具體事如何實現數據雙向綁定的。只要知道他經過髒檢查來實現的,須要主動去觸發一些事件才能產生。要想進入$digest cycle:
要知足:
到此爲止,說了不少不須要了解的東西,下面的篇章不會這麼廢話了。