Angular之雙向資料繫結(下)

來源:互聯網
上載者:User

標籤:

本篇詳細介紹: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之雙向資料繫結(下)

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.