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/

相关文章
相关标签/搜索