標籤:
本篇詳細介紹:1.angular時如何通過髒檢查來實現對$scope對象上變數的雙向繫結的。2.實現angular雙向繫結的三個重要方法:$digest(),$apply(),$watch().
angular不像Ember.js,通過動態設定setter函數和getter函數來實現雙向繫結,髒檢查允許angular監聽可能存在可能不存在的變數。
$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源碼中,
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函數。
下面舉個應用執行個體,以插值{{post.title}}為例,當angular在compile編譯階段遇到這個文法元素時,內部處理邏輯如下:
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內部,會發現它這樣的一個數組。
$$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這個函數來停止監聽。
我是萌萌嗒分割線
在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:
要滿足:
- DOM事件,譬如使用者輸入文本,點擊按鈕等。(ng-click)
- XHR響應事件 ($http)
- 瀏覽器Location變更事件 ($location)
- Timer事件($timeout, $interval)
- 執行$digest()或$apply()
到此為止,說了很多不需要瞭解的東西,下面的篇章不會這麼廢話了。
Angular之雙向資料繫結(下)