Angularjs雙向數據綁定是如何實現的

AngularJS數據雙向綁定揭祕

AngularJS在$scope變量中使用髒值檢查來實現了數據雙向綁定。和Ember.js數據雙向綁定中動態設施setter和getter不一樣,髒治檢查容許AngularJS監視那些存在或者不存在的變量。javascript

$scope.$watch

$scope.$watch( watchExp, listener, objectEquality );html

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

$scope.name = 'Ryan';

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

AngularJS將會在$scope中註冊你的監視函數。你能夠在控制檯中輸出$scope來查看$scope中的註冊項目。angularjs

你能夠在控制檯中看到$scope.name已經發生了變化 – 這是由於$scope.name以前的值彷佛undefined而如今咱們將它賦值爲Ryan!web

對於$wach的第一個參數,你也可使用一個字符串。這和提供一個函數徹底同樣。在AngularJS的源代碼中能夠看到,若是你使用了一個字符串,將會運行下面的代碼:數組


這將會把咱們的watchExp設置爲一個函數,它也自動返回做用域中咱們已經制定了名字的變量。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); }; }

$$watchers

$scope中的$$watchers變量保存着咱們定義的全部的監視器。若是你在控制檯中查看$$watchers,你會發現它是一個對象數組。app

$$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這個函數,要用的時候記得給它傳遞一個參數。this

下面咱們來看看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神祕外衣之下數據雙向綁定的祕密!


本文參考自How AngularJS implements dirty checking and how to replicate it ourselves,原文地址http://ryanclark.me/how-angularjs-implements-dirty-checking/

相關文章
相關標籤/搜索