我的angularjs源碼學習之旅3——臟檢測與數據雙向綁定


前言


   為了后面描述方便,我們將保存模塊的對象modules叫做模塊緩存。我們跟蹤的例子如下

  <div ng-app="myApp" ng-controller='myCtrl'>
      <input type="text" ng-model='name'/>
      <span style='width: 100px;height: 20px;    margin-left: 300px;'>{{name}}</span>
  </div>
  <script>
  var app = angular.module('myApp', []);
  app.controller('myCtrl', function($scope) {
      $scope.name = 1;
  });
  </script>

  在angular初始化中,在執行完下面代碼后

publishExternalAPI(angular);
angular.module("ngLocale", [], ["$provide", function($provide) {...}]);

  模塊緩存中保存着有兩個模塊

modules = {
  ng:{
    _invokeQueue: [], 
    _configBlocks:[["$injector","invoke",[["$provide",ngModule($provide)]]]], 
    _runBlocks: [], 
    name: "ng",
    requires: ["ngLocale"],
    ...
  },
  ngLocale: {
    _invokeQueue: [],
    _configBlocks: [["$injector","invoke",[["$provide", anonymous($provide)]]]],    
    _runBlocks: [],
    name: "ngLocale",
    requires: [],
    ...
  }
}

  每個模塊都有的下面的方法,為了方便就沒有一一列出,只列出了幾個關鍵屬性

  animation: funciton(recipeName, factoryFunction),
  config: function(),
  constant: function(),
  controller: function(recipeName, factoryFunction),
  decorator: function(recipeName, factoryFunction),
  directive: function(recipeName, factoryFunction),
  factory: function(recipeName, factoryFunction),
  filter: function(recipeName, factoryFunction),  
  provider: function(recipeName, factoryFunction),  
  run: function(block),
  service: function(recipeName, factoryFunction),
  value: function()

  然后執行到我們自己寫的添加myApp模塊的代碼,添加一個叫myApp的模塊

modules = {
  ng:{ ...  },
  ngLocale: {...  },
  myApp: {
_invokeQueue: [],
_configBlocks: [],
_runBlocks: [],
name: "ngLocale",
requires: [],
...
}
}

   執行 app.controller('myCtrl', function($scope) {})的源碼中會給該匿名函數添加.$$moduleName屬性以確定所屬模塊,然后往所屬模塊的_invokeQueue中壓入執行代碼等待出發執行。

function(recipeName, factoryFunction) {
  if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
  invokeQueue.push([provider, method, arguments]);
  return moduleInstance;
};

  然后等到頁面加載完成后,bootstrap函數調用中調用了這段代碼,傳入的參數modules為["ng", ["$provide",function($provide)], "myApp"]

var injector = createInjector(modules, config.strictDi);

  初始化依賴注入對象,里面用到loadModules函數,其中有這段代碼

function loadModules(modulesToLoad) {
  ...
  forEach(modulesToLoad, function(module) {
     ...
      function runInvokeQueue(queue) {
        var i, ii;
        for (i = 0, ii = queue.length; i < ii; i++) {
          var invokeArgs = queue[i],
              provider = providerInjector.get(invokeArgs[0]);

          provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
        }
      }

      try {
        if (isString(module)) {
          moduleFn = angularModule(module);
          runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
          runInvokeQueue(moduleFn._invokeQueue); runInvokeQueue(moduleFn._configBlocks);
        }
        ...
      }
  });
}

  先前在app.controller('myCtrl', function($scope) {})中向myApp模塊的_invokeQueue中添加了等待執行的代碼

_invokeQueue = [["$controllerProvider","register",["myCtrl",function($scope)]]]

  現在執行之,最后在下面函數中給當前模塊的內部變量controllers上添加一個叫"myCtrl"的函數屬性。

  this.register = function(name, constructor) {
    assertNotHasOwnProperty(name, 'controller');
    if (isObject(name)) {
      extend(controllers, name);
    } else {
      controllers[name] = constructor;
    }
  };

  

執行bootstrapApply


   執行

injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
       function bootstrapApply(scope, element, compile, injector) {
        scope.$apply(function() {
          element.data('$injector', injector);
          compile(element)(scope);
        });
      }]
    );

  執行該段代碼之前的instanceCache是

cache = {
  $injector: {
    annotate: annotate(fn, strictDi, name),
    get: getService(serviceName, caller),
    has: anonymus(name),
    instantiate: instantiate(Type, locals, serviceName),
    invoke: invoke(fn, self, locals, serviceName)
  }
}

  執行到調用function bootstrapApply(scope, element, compile, injector) {}之前變成了

cache = {
  $$AnimateRunner: AnimateRunner(),
  $$animateQueue: Object,
  $$cookieReader: (),
  $$q: Q(resolver),
  $$rAF: (fn),
  $$sanitizeUri: sanitizeUri(uri, isImage),
  $animate: Object,
  $browser: Browser,
  $cacheFactory: cacheFactory(cacheId, options),
  $compile: compile($compileNodes, transcludeFn, maxPriority, ignoreDirective,previousCompileContext),
  $controller: (expression, locals, later, ident),
  $document: JQLite[1],
  $exceptionHandler: (exception, cause),
  $filter: (name),
  $http: $http(requestConfig),
  $httpBackend: (method, url, post, callback, headers, timeout, withCredentials, responseType),
  $httpParamSerializer: ngParamSerializer(params),
  $injector: Object,
  $interpolate: $interpolate(text, mustHaveExpression, trustedContext, allOrNothing),
  $log: Object,
  $parse: $parse(exp, interceptorFn, expensiveChecks),
  $q: Q(resolver),
  $rootElement: JQLite[1],
  $rootScope: Scope,
  $sce: Object,
  $sceDelegate: Object,
  $sniffer: Object,
  $templateCache: Object,
  $templateRequest: handleRequestFn(tpl, ignoreRequestError),
  $timeout: timeout(fn, delay, invokeApply),
  $window: Window
}

  而且獲取到了應用的根節點的JQLite對象傳入bootstrapApply函數。

compile中調用var compositeLinkFn = compileNodes(...)編譯節點,主要迭代編譯根節點的后代節點

  childLinkFn = (nodeLinkFn && nodeLinkFn.terminal ||
                      !(childNodes = nodeList[i].childNodes) ||
                      !childNodes.length)
            ? null
            : compileNodes(childNodes,
                 nodeLinkFn ? (
                  (nodeLinkFn.transcludeOnThisElement || !nodeLinkFn.templateOnThisElement)
                     && nodeLinkFn.transclude) : transcludeFn);

  每一級的節點的處理由每一級的linkFns數組保存起來,並在每一級的compositeLinkFn函數中運用,linkFns的結構是[index, nodeLinkFn, childLinkFn]。

  最終返回一個復合的鏈接函數。  

compile.$$addScopeClass($compileNodes);給應用根節點加上一個"ng-scope"的class

最后compile(element)返回一個函數publicLinkFn(這個函數很多外部變量就是已經編譯好的節點),然后將當前上下文環境Scope代入進這個函數。

publicLinkFn函數中給節點以及后代節點添加了各自的緩存;

接下來是進入$rootScope.$digest();執行數據的臟檢測和數據的雙向綁定。

  

下面一小點是angular保存表達式的方法:

  標簽中的表達式被$interpolate函數解析,普通字段和表達式被切分開放在concat中。比如

<span>名稱是{{name}}</span>

  解析后的concat為["名稱是", ""],而另一個變量expressionPositions保存了表達式在concat的位置(可能有多個),此時expressionPositions為[1],當臟檢測成功后進入compute計算最終值的時候循環執行concat[expressionPositions[i]] = values[i];然后將concat內容拼接起來設置到DOM對象的nodeValue。

function interpolateFnWatchAction(value) {
  node[0].nodeValue = value;
});

 

臟檢測與數據雙向綁定


   我們用$scope表示一個Scope函數的實例。

$scope.$watch( watchExp, listener[, objectEquality]);

  注冊一個監聽函數(listener)當監聽的表達式(watchExp)發生變化的時候執行監聽函數。objectEquality是布爾值類型,確定監聽的內容是否是一個對象。watchExp可以是字符串和函數。

  我們在前面的例子的控制器中添加一個監聽器

      $scope.name = 1;

      $scope.$watch( function( ) {
        return $scope.name;
      }, function( newValue, oldValue ) {
        alert('$scope.name 數據從' + oldValue + "改成了" + newValue);//$scope.name 數據從1改成了1
      });

  當前作用域的監聽列表是有$scope.$$watchers保存的,比如現在我們當前添加了一個監聽器,其結構如下

$scope.$$watchers = [
    {
        eq: false, //是否需要檢測對象相等
        fn: function( newValue, oldValue ) {alert('$scope.name 數據從' + oldValue + "改成了" + newValue);}, //監聽器函數
        last: function initWatchVal(){}, //最新值
        exp: function(){return $scope.name;}, //watchExp函數
        get: function(){return $scope.name;} //Angular編譯后的watchExp函數
    }
];

  除了我們手動添加的監聽器外,angular會自動添加另外兩個監聽器($scope.name變化修改其相關表達式的監聽器和初始化時從模型到值修正的監聽器)。最終有三個監聽器。需要注意的是最用運行的時候是從后往前遍歷監聽器,所以先執行的是手動添加的監聽器,最后執行的是數據雙向綁定的監聽器(//監聽input變化修改$scope.name以及其相關聯的表達式的監聽器)

  $scope.$$watchers = [
    {//監聽$scope.name變化修改其相關聯的表達式的監聽器
      eq: false,
      exp: regularInterceptedExpression(scope, locals, assign, inputs),
      fn: watchGroupAction(value, oldValue, scope),
      get: expressionInputWatch(scope),
      last: initWatchVal()
    },
    {//從模型到值修正的監聽器
      eq: false,
      exp: ngModelWatch(),
      fn: noop(),
      get: ngModelWatch(),
      last: initWatchVal()
    },
    {//手動添加的監聽$scope.name變化的監聽器
      eq: false, //是否需要檢測對象相等
      fn: function( newValue, oldValue ) {alert('$scope.name 數據從' + oldValue + "改成了" + newValue);}, //監聽器函數
      last: initWatchVal(){}, //最新值
      exp: function(){return $scope.name;}, //watchExp函數
      get: function(){return $scope.name;} //Angular編譯后的watchExp函數
    }
  ]

   第二個監聽器有點特殊,他是使用$scope.$watch(function ngModelWatch() {...});監聽的,只有表達式而沒有監聽函數。官方的解釋是:函數監聽模型到值的轉化。我們沒有使用正常的監聽函數因為要檢測以下幾點:

  1.作用域值為‘a'

  2.用戶給input初始化的值為‘b’

  3.ng-change應當被啟動並還原作用域值'a',但是此時作用域值並沒有發生改變(所以在應用階段最后一次臟檢測作為ng-change監聽事件執行)

  4. 視圖應該恢復到'a'

  這個監聽器在初始化的時候判斷input的值和$scope.name是否相同,不同則用$scope.name替換之。源碼如下

  $scope.$watch(function ngModelWatch() {
    var modelValue = ngModelGet($scope);

    // if scope model value and ngModel value are out of sync
    // TODO(perf): why not move this to the action fn?
    if (modelValue !== ctrl.$modelValue &&
       // checks for NaN is needed to allow setting the model to NaN when there's an asyncValidator
       (ctrl.$modelValue === ctrl.$modelValue || modelValue === modelValue)
    ) {
      ctrl.$modelValue = ctrl.$$rawModelValue = modelValue;
      parserValid = undefined;

      var formatters = ctrl.$formatters,
          idx = formatters.length;

      var viewValue = modelValue;
      while (idx--) {
        viewValue = formatters[idx](viewValue);
      }
      if (ctrl.$viewValue !== viewValue) {
        ctrl.$viewValue = ctrl.$$lastCommittedViewValue = viewValue;
        ctrl.$render();

        ctrl.$$runValidators(modelValue, viewValue, noop);
      }
    }

    return modelValue;
  });

  這個也是實現數據雙向綁定的原因,每次$scope.name做了更改都會執行到這個監聽器,監聽器里面判斷當前作用域的值和DOM元素中的值是否相同,如果不同則給視圖渲染作用域的值。

  $watch返回一個叫做deregisterWatch的函數,顧名思義,你可以通過這個函數來解除當前的這個監聽。

$scope.$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(往往是函數);最后執行$rootScope.$digest();用我的理解來說實際就是一個啟動臟值檢測的。可能還有一個用處就是加了一個正在執行臟值檢測的標志,有些地方會判斷當前是否在執行臟值檢測從而啟動異步執行來保障臟值檢測先執行完畢。

   $scope.$apply應該在事件觸發的時候調用。$scope.$watch雖然保存着有監聽隊列,但是這些監聽隊列是如何和DOM事件關聯起來的呢?原來在編譯節點的時候angular就給不通的節點綁定了不同的事件,比如基本的input標簽通過baseInputType來綁定事件

function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
  ...
  if (!$sniffer.android) {
    var composing = false;

    element.on('compositionstart', function(data) {
      composing = true;
    });

    element.on('compositionend', function() {
      composing = false;
      listener();
    });
  }
  ...
  if ($sniffer.hasEvent('input')) {
    element.on('input', listener);
  } else {
    ...
    element.on('keydown', function(event) {...});

    if ($sniffer.hasEvent('paste')) {
      element.on('paste cut', deferListener);
    }
  }

  element.on('change', listener);
 ...
}

 

 $rootScope.$digest()

   我們發現臟值檢測函數$digest始終是在$rootScope中被$scope.$apply所調用。然后向下遍歷每一個作用域並在每個作用域上運行循環。所謂的臟值就是值被更改了。當$digest遍歷到某一個作用域的時候,檢測該作用域下$$watchers中的監聽事件,遍歷之並對比新增是否是臟值,如果是則觸發對應的監聽事件。

      $digest: function() {
        var watch, value, last,
            watchers,
            length,
            dirty, ttl = TTL,
            next, current, target = this,
            watchLog = [],
            logIdx, logMsg, asyncTask;

        beginPhase('$digest');
        // Check for changes to browser url that happened in sync before the call to $digest
        $browser.$$checkUrlChange();

        if (this === $rootScope && applyAsyncId !== null) {
          // If this is the root scope, and $applyAsync has scheduled a deferred $apply(), then
          // cancel the scheduled $apply and flush the queue of expressions to be evaluated.
          $browser.defer.cancel(applyAsyncId);
          flushApplyAsync();
        }

        lastDirtyWatch = null;

        do { // "while dirty" loop
          dirty = false;
          current = target;
          ...

          traverseScopesLoop:
          do { //遍歷作用域
            if ((watchers = current.$$watchers)) {
              // process our watches
              length = watchers.length;
              while (length--) {
                try {
                  watch = watchers[length];
                  // 大部分監聽都是原始的,我們只需要使用===比較即可,只有部分需要使用.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);//執行監聽函數 ...
                  }
                } catch (e) {
                  $exceptionHandler(e);
                }
              }
            }

            // 瘋狂警告: 作用域深度優先遍歷
            // 使得,這段代碼有點瘋狂,但是它有用並且我們的測試證明其有用
            // 在$broadcast遍歷時這個代碼片段應當保持同步
            if (!(next = ((current.$$watchersCount && current.$$childHead) ||
                (current !== target && current.$$nextSibling)))) {
              while (current !== target && !(next = current.$$nextSibling)) {
                current = current.$parent;
              }
            }
          } while ((current = next));

          // `break traverseScopesLoop;` takes us to here
          if ((dirty || asyncQueue.length) && !(ttl--)) {
            clearPhase();
            throw $rootScopeMinErr('infdig',
                '{0} $digest() iterations reached. Aborting!\n' +
                'Watchers fired in the last 5 iterations: {1}',
                TTL, watchLog);
          }

        } while (dirty || asyncQueue.length);

        clearPhase();

        while (postDigestQueue.length) {
          try {
            postDigestQueue.shift()();
          } catch (e) {
            $exceptionHandler(e);
          }
        }
      },

   至於數據的雙向綁定。我們在綁定監聽事件的處理函數中就已經有對$scope.name指的修改(有興趣的可以去跟蹤一下)這是其中一個方向的綁定。監聽器的最前面兩個監聽器就保證了數據的反向綁定。第二個監聽器保證了作用域的值和DOM的ng-modle中的值一致。第一個監聽器則保證作用域的值和DOM的表達式的值一致。

  OK,angular的臟值檢測和數據雙向綁定分析就到這里。不足之處請見諒,不對的地方請各位大牛指出。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM