【原創】angularjs1.3.0源碼解析之執行流程


Angular執行流程

前言

發現最近angularjs在我廠的應用變得很廣泛,下周剛好也有個angular項目要着手開始做,所以先做了下功課,從源代碼開始入手會更深刻點,可能講的沒那么細,側重點在於整個執行流程,之后會對angularjs的各個模塊再解析下。

目錄結構

angularFiles.js中就可以看到angular.js是如何打包的?

  'angularSrc': [
    'src/minErr.js',
    'src/Angular.js',
    'src/loader.js',
    'src/AngularPublic.js',
    'src/jqLite.js',
    'src/apis.js',

    'src/auto/injector.js',

    'src/ng/anchorScroll.js',
    'src/ng/animate.js',
    'src/ng/asyncCallback.js',
    // ...省略若干service
	
    'src/ng/filter.js',
    'src/ng/filter/filter.js',
    'src/ng/filter/filters.js',
    // ...省略若干filter

    'src/ng/directive/directives.js',
    'src/ng/directive/a.js',
    'src/ng/directive/attrs.js',
    // ...省略若干directive
  ]

核心代碼(angular.js)

這里精簡了代碼,省略了angular很多的方法定義,直接從執行流程入手,看angular如何進行一個程序的初始化。

(function(window, document, undefined) {

  // ...省略若干代碼
  
  // 判斷代碼angularjs重復加載
  if (window.angular.bootstrap) {
       console.log('WARNING: Tried to load angular more than once.');
    return;
  }

  // 綁定jQuery或者jqLite,實現angular.element  
  bindJQuery();

  // 暴露api,掛載一些通用方法,如:angular.forEach
  // 實現angular.module,並定義模塊ng,以及ngLocale
  publishExternalAPI(angular);

  // 當dom ready時,開始執行程序的初始化
  jqLite(document).ready(function() {
  	// 初始化入口
    angularInit(document, bootstrap);
  });

})(window, document);

publishExternalAPI函數(src/AngularPublic.js)

  1. 將一些通用方法掛載到全局變量angular上
  2. 注冊兩個模塊ngngLocale(其中ng依賴ngLocale)
  3. ng模塊的回調函數用來注冊angular內置的servicedirective(該回調將在angularInit后被執行)
function publishExternalAPI(angular){
  // 將通用方法掛載到全局變量augular上
  extend(angular, {
    'bootstrap': bootstrap,
    'copy': copy,
    'extend': extend,
    'equals': equals,
    'element': jqLite,
    'forEach': forEach,
    'injector': createInjector,
    // ...省略若干通用方法
  });

  // 實現angular.module方法並賦值給angularModule
  angularModule = setupModuleLoader(window);

  try {
    // 獲取ngLocale模塊
    // 如果獲取不到,則會報出異常
    angularModule('ngLocale');
  } catch (e) {
    // 接受異常(也就是沒有獲取不到ngLocale模塊)
    // 在這里注冊ngLocale模塊
    angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
  }

  // 注冊ng模塊(此模塊依賴ngLocale模塊)
  // 回調中注冊N多service,以及N多directive(回調等待初始化angularInit后執行)
  angularModule('ng', ['ngLocale'], ['$provide',
    function ngModule($provide) {
      // $$sanitizeUriProvider needs to be before $compileProvider as it is used by it.
      $provide.provider({
        $$sanitizeUri: $$SanitizeUriProvider
      });
      $provide.provider('$compile', $CompileProvider).
        directive({
            a: htmlAnchorDirective,
            input: inputDirective,
            textarea: inputDirective,
            form: formDirective,
            // ...省略若干directive
        }).
        directive({
          ngInclude: ngIncludeFillContentDirective
        }).
        directive(ngAttributeAliasDirectives).
        directive(ngEventDirectives);
      $provide.provider({
        $anchorScroll: $AnchorScrollProvider,
        $animate: $AnimateProvider,
        $browser: $BrowserProvider,
        // ...省略若干service
      });
    }
  ]);
}

setupModuleLoader函數(src/loader.js)

這里定義了angular.module方法,該方法用來注冊模塊並返回模塊實例,像上面所說的ngngLocale模塊都是通過它注冊的。

function setupModuleLoader(window) {

  // 異常處理,可以忽略
  var $injectorMinErr = minErr('$injector');
  var ngMinErr = minErr('ng');

  // 獲取指定obj的name屬性
  // 不存在的話,利用factory函數創建並存儲在obj.name下,方便下次獲取
  function ensure(obj, name, factory) {
    return obj[name] || (obj[name] = factory());
  }

  // 獲取angular全局變量
  var angular = ensure(window, 'angular', Object);

  angular.$$minErr = angular.$$minErr || minErr;

  // 定義angular.module方法並返回
  return ensure(angular, 'module', function() {

    // 利用閉包,緩存模塊實例
    var modules = {};

    // angular.module 的方法實現
    // 如果參數是一個,獲取指定name的模塊(getter操作)
    // 否則,(重新)創建模塊實例並存儲(setter操作),最后返回
    return function module(name, requires, configFn) {
    
      // 檢測模塊名不能是'hasOwnProperty'
      var assertNotHasOwnProperty = function(name, context) {
        if (name === 'hasOwnProperty') {
          throw ngMinErr('badname', 'hasOwnProperty is not a valid {0} name', context);
        }
      };
      
      // 檢測模塊名
      assertNotHasOwnProperty(name, 'module');
      
      // 如果參數不止一個(requires存在,哪怕是[]),表現為setter操作
      // 如果該模塊已存在,那么置為null,重新創建
      if (requires && modules.hasOwnProperty(name)) {
        modules[name] = null;
      }
      
      // 獲取指定name模塊的模塊
      // 從modules緩存中取或者(重新)創建新的模塊實例
      return ensure(modules, name, function() {
      
        // 程序走到這里,表示是新模塊的創建過程
        // 而requires如果為空,則表示是獲取已有的模塊
        // 兩者其實是相互矛盾的,所以拋出異常,說明該模塊還沒有注冊
        // 所以我們在創建模塊時,就算沒有依賴其他模塊,寫法也應該是:
        // angular.module('myModule', []);
        if (!requires) {
          throw $injectorMinErr('nomod', "Module '{0}' is not available! You either misspelled " +
             "the module name or forgot to load it. If registering a module ensure that you " +
             "specify the dependencies as the second argument.", name);
        }

        var invokeQueue = [];

        var configBlocks = [];

        var runBlocks = [];

        var config = invokeLater('$injector', 'invoke', 'push', configBlocks);

        // 將要返回的module實例
        var moduleInstance = {
        
          _invokeQueue: invokeQueue,
          _configBlocks: configBlocks,
          _runBlocks: runBlocks,

          requires: requires,

          name: name,

          provider: invokeLater('$provide', 'provider'),

          factory: invokeLater('$provide', 'factory'),

          service: invokeLater('$provide', 'service'),

          value: invokeLater('$provide', 'value'),

          constant: invokeLater('$provide', 'constant', 'unshift'),

          animation: invokeLater('$animateProvider', 'register'),

          filter: invokeLater('$filterProvider', 'register'),

          controller: invokeLater('$controllerProvider', 'register'),

          directive: invokeLater('$compileProvider', 'directive'),

          config: config,

          run: function(block) {
            runBlocks.push(block);
            return this;
          }
        };

        if (configFn) {
          config(configFn);
        }

        return  moduleInstance;

        // 通過該方法對module實例的一系列常用方法進行包裝,如myModule.provider,myModule.controller
        // 我們在調用myModule.provider(...)時實質上是數據存儲(push或者unshift)而不是立即注冊服務
        // 這一點我們從invokeLater的字面意思(之后再調用)也可以看出
        // 那么真正的執行(如注冊服務),是在angularInit之后,准確的說是在loadModules的時候(之后會說到)
        function invokeLater(provider, method, insertMethod, queue) {
        
          // 默認隊列是invokeQueue,也可以是configBlocks
          // 默認隊列操作是push,也可以是unshift
          if (!queue) queue = invokeQueue;
          return function() {
            queue[insertMethod || 'push']([provider, method, arguments]);
            return moduleInstance;
          };
        }
      });
    };
  });

}

angularInit函數(src/Angular.js)

當dom ready時,該函數開始運行,通過調用bootstrap函數進行整個angular應用的初始化工作。
這里傳遞給bootstrap兩個函數:應用根節點(含有xx-app屬性的dom)和啟動模塊(xx-app的值)
xx 為 ['ng-', 'data-ng-', 'ng:', 'x-ng-'] 任一一種,這里做了兼容多種屬性名

function angularInit(element, bootstrap) {
  var appElement,
      module,
      config = {};

  // ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];
  // 支持多種節點屬性表達方式,通過用循環方式查找含有xx-app屬性的節點(appElement)
  // 先對element(即document)進行判斷,再從element中的子孫元素中查找
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';

    if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
      appElement = element;
      module = element.getAttribute(name);
    }
  });
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';
    var candidate;

    if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) {
      appElement = candidate;
      module = candidate.getAttribute(name);
    }
  });
  
  // 如果應用節點存在,那么啟動整個應用(即bootstrap)
  // 如果appElement含有xx-strict-di屬性,那么設置嚴格依賴注入參數?
  if (appElement) {
    config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
    bootstrap(appElement, module ? [module] : [], config);
  }
}

bootstrap函數(src/Angular.js)

程序的核心初始化起始於該函數
除了通過ng-app指令自動初始化應用(間接調用bootstrap)外,我們也可以手動調用angular.bootstrap(...)來初始化應用

比如像這樣:

angular.module('demo', [])
    .controller('WelcomeController', function($scope) {
         $scope.greeting = 'Welcome!';
    });
angular.bootstrap(document, ['demo']);

看看源碼(這里我們只看下里面的核心函數doBootstrap):

function bootstrap(element, modules, config) {
  // ...省略若干代碼
  var doBootstrap = function() {
    element = jqLite(element);

    // 首先判斷該dom是否已經被注入(即這個dom已經被bootstrap過)
    // 注意這里的injector方法是Angular為jqLite中提供的,區別於一般的jQuery api
    if (element.injector()) {
      var tag = (element[0] === document) ? 'document' : startingTag(element);
      //Encode angle brackets to prevent input from being sanitized to empty string #8683
      throw ngMinErr(
          'btstrpd',
          "App Already Bootstrapped with this Element '{0}'",
          tag.replace(/</,'&lt;').replace(/>/,'&gt;'));
    }

    modules = modules || [];
    
    // 添加匿名模塊
    modules.unshift(['$provide', function($provide) {
      $provide.value('$rootElement', element);
    }]);

    if (config.debugInfoEnabled) {
      // Pushing so that this overrides `debugInfoEnabled` setting defined in user's `modules`.
      modules.push(['$compileProvider', function($compileProvider) {
        $compileProvider.debugInfoEnabled(true);
      }]);
    }

    // 添加ng模塊
    modules.unshift('ng');
    
    // 到這里modules可能是: ['ng', [$provide, function($provide){...}], 'xx']
    // xx: ng-app="xx" 
    
    // 創建injector對象,注冊所有的內置模塊
    var injector = createInjector(modules, config.strictDi);
    
    // 利用injector的依賴注入,執行回調
    injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
       function bootstrapApply(scope, element, compile, injector) {
        scope.$apply(function() {
          // 標記該dom已經被注入
          element.data('$injector', injector);
          // 編譯整個dom
          compile(element)(scope);
        });
      }]
    );
    return injector;
  };

  // ...省略若干代碼
  
  if (window && !NG_DEFER_BOOTSTRAP.test(window.name)) {
    return doBootstrap();
  }
  // ...省略若干代碼
}

createInjector函數(src/auto/injector.js)

實現依賴注入,創建injector實例。

function createInjector(modulesToLoad, strictDi) {
  strictDi = (strictDi === true);
  var INSTANTIATING = {},
      providerSuffix = 'Provider',
      path = [],
      loadedModules = new HashMap([], true),
      providerCache = {
        $provide: {
            provider: supportObject(provider),
            factory: supportObject(factory),
            service: supportObject(service),
            value: supportObject(value),
            constant: supportObject(constant),
            decorator: decorator
          }
      },
      providerInjector = (providerCache.$injector =
          createInternalInjector(providerCache, function() {
            throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
          })),
      instanceCache = {},
      instanceInjector = (instanceCache.$injector =
          createInternalInjector(instanceCache, function(servicename) {
            var provider = providerInjector.get(servicename + providerSuffix);
            return instanceInjector.invoke(provider.$get, provider, undefined, servicename);
          }));

  // 循環加載模塊,實質上是:
  // 1. 注冊每個模塊上掛載的service(也就是_invokeQueue)
  // 2. 執行每個模塊的自身的回調(也就是_configBlocks)
  // 3. 通過依賴注入,執行所有模塊的_runBlocks
  forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });

  return instanceInjector;
  
  // ...省略若干函數定義
 }

上面定義了4個變量,分別是:

  • providerCache(所有xxProvider類的緩存,xx可以是Locale,Timeout)
  • instanceCache(所有xxProvider返回的實例緩存)
  • providerInjector(內部injector實例,負責類層級的依賴注入)
  • instanceInjector(外部可訪問injector實例,負責實例層級的依賴注入)

這里providerCache預存了$provider服務類,用來提供自定義service的注冊,支持下面幾個方法:

$provide: {
    provider: supportObject(provider),
    factory: supportObject(factory),
    service: supportObject(service),
    value: supportObject(value),
    constant: supportObject(constant),
    decorator: decorator
}

之后providerCache.$injector=createInternalInjector(...);又將$injector服務緩存進來(其實之后注冊的服務都將陸續添加進來)

createInternalInjector函數(src/auto/injector.js)

創建injector實例,如:providerInjectorinstanceInjector

function createInternalInjector(cache, factory) {

  function getService(serviceName) {/*省略代碼實現*/}

  function invoke(fn, self, locals, serviceName) {/*省略代碼實現*/}

  function instantiate(Type, locals, serviceName) {/*省略代碼實現*/}

  // 返回injector實例
  return {
    invoke: invoke,
    instantiate: instantiate,
    get: getService,
    annotate: annotate,
    has: function(name) {
      return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
    }
  };
}

loadModules函數(src/auto/injector.js)

加載應用依賴模塊以及內置的ng模塊等,就像之前說的類似這樣:['ng', [$provide, function($provide){...}], 'xx']
執行每個模塊的_runBlocks,可以理解injector創建完后模塊的初始化(通過myModule.run(...)注冊的)

function loadModules(modulesToLoad){
    var runBlocks = [], moduleFn;
    
    // 循環加載每個module,
    // 1. 注冊每個模塊上掛載的service(也就是_invokeQueue)
    // 2. 執行每個模塊的自身的回調(也就是_configBlocks)
    // 3. 通過遞歸搜集所有(依賴)模塊的_runBlocks,並返回
    forEach(modulesToLoad, function(module) {
    
      // 判斷模塊是否已經加載過
      if (loadedModules.get(module)) return;
      
      // 設置模塊已經加載過
      loadedModules.put(module, true);

      function runInvokeQueue(queue) {
        var i, ii;
        for(i = 0, ii = queue.length; i < ii; i++) {
        
          var invokeArgs = queue[i],
              provider = providerInjector.get(invokeArgs[0]);
              
          // 通過providerInjector獲取指定服務(類),傳遞參數並執行指定方法
          provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
        }
      }

      // 模塊可以是以下三種情況:
      // 1. 字符串表示模塊名(注冊過的模塊),如:'ng'模塊
      // 2. 普通函數(也可以是隱式聲明依賴的函數),如:function($provider) {...}
      // 3. 數組(即聲明依賴的函數)如:[$provide, function($provide){...}
      try {
        if (isString(module)) {
          // 獲取通過模塊名獲取模塊對象
          moduleFn = angularModule(module);
          // 通過遞歸加載所有依賴模塊,並且獲取所有依賴模塊(包括自身)的_runBlocks
          runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
          // 遍歷_invokeQueue數組依次執行$provider服務的指定方法(如:factory,value等)
          runInvokeQueue(moduleFn._invokeQueue);
          // 遍歷_configBlocks數組依次執行$injector服務的invoke方法(即依賴注入並執行回調)
          runInvokeQueue(moduleFn._configBlocks);
          
        // 如果module是函數或者數組(可認為是匿名模塊),那么依賴注入后直接執行
        // 並將返回值保存到runBlocks(可能是函數,又將繼續執行)
        } else if (isFunction(module)) {
            runBlocks.push(providerInjector.invoke(module));
        } else if (isArray(module)) {
            runBlocks.push(providerInjector.invoke(module));
        } else {
          assertArgFn(module, 'module');
        }
      } catch (e) {
        if (isArray(module)) {
          module = module[module.length - 1];
        }
        if (e.message && e.stack && e.stack.indexOf(e.message) == -1) {
          e = e.message + '\n' + e.stack;
        }
        throw $injectorMinErr('modulerr', "Failed to instantiate module {0} due to:\n{1}",
                  module, e.stack || e.message || e);
      }
    });

    return runBlocks;
  }  

到這里在注冊ng模塊時的回調,在runInvokeQueue(moduleFn._configBlocks);已經執行過了,也就意味着許許多多的內置模塊已經存入providerCache中了,所以在后面的依賴注入中我們可以隨意調用。

ps: 不知道是不是好久沒用博客園了,感覺它的markdown好難用!!原文在這里


免責聲明!

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



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