• 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()

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

  • 相关阅读:
    <mvc:annotation-driven>新增标签
    关于Spring中的<context:annotation-config/>配置
    <mvc:default-servlet-handler/>的作用
    各种WEB服务器自带的默认Servlet名称
    常用邮件协议
    vue-cli 脚手架项目简介(一)
    CSS3的transition和transform
    Spring配置文件<context:property-placeholder>标签使用漫谈
    使用Spring JDBCTemplate简化JDBC的操作
    技术探索不易
  • 原文地址:https://www.cnblogs.com/brancepeng/p/5011804.html
Copyright © 2020-2023  润新知