Angular之双向数据绑定(下)

本篇详细介绍: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:

要知足:

  • DOM事件,譬如用户输入文本,点击按钮等。(ng-click)
  • XHR响应事件 ($http)
  • 浏览器Location变动事件 ($location)
  • Timer事件($timeout, $interval)
  • 执行$digest()或$apply()

到此为止,说了不少不须要了解的东西,下面的篇章不会这么废话了。

相关文章
相关标签/搜索