zoukankan      html  css  js  c++  java
  • 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()

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

  • 相关阅读:
    机器学习(深度学习)
    机器学习(六)
    机器学习一-三
    Leetcode 90. 子集 II dfs
    Leetcode 83. 删除排序链表中的重复元素 链表操作
    《算法竞赛进阶指南》 第二章 Acwing 139. 回文子串的最大长度
    LeetCode 80. 删除有序数组中的重复项 II 双指针
    LeetCode 86 分割链表
    《算法竞赛进阶指南》 第二章 Acwing 138. 兔子与兔子 哈希
    《算法竞赛进阶指南》 第二章 Acwing 137. 雪花雪花雪花 哈希
  • 原文地址:https://www.cnblogs.com/brancepeng/p/5011804.html
Copyright © 2011-2022 走看看