Angular之双向数据绑定(下)

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

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

posted @ 2015-12-02 01:13  xiaoxiao彭  阅读(1966)  评论(0编辑  收藏  举报