AngularJS源码阅读1:启动过程

AngularJS 确实是一个比较强大的框架,如何深入理解它然后写出比较漂亮的组件确实是一门学问,虽然目前实验室的项目中有使用它,总觉得不够完美,今日决定读一读源码。

这里我选用了最新的1.4.0的源码,作为第一篇就先来看看 AngularJS 是怎么启动的吧。

初始化

AngularJS 加载之后,就会有一段立即执行的初始化代码,请看第28121行之后:

// 绑定jQuery,后面的jqLite(document).ready用到
// 启动的时候会再次绑定
bindJQuery();

publishExternalAPI(angular);

jqLite(document).ready(function() {  
    angularInit(document, bootstrap);
});

初始化就只用三个函数,下面分别来看一看:

bindJQuery

这个方法的作用是:检查主页面是否引用了 jQuery,没有的话,就用Angular自带的 JQLite,否则使用引用的 jQuery。如果要在Angular中使用 jQuery,只需要在Angular之前引入 jQuery 即可。

// 是否已经绑定过jQuery
var bindJQueryFired = false;

function bindJQuery() {  
  // 已经绑定过,不再绑定
  if (bindJQueryFired) {
    return;
  }
  //如果在主页面加载了jQuery库,这里的jQuery就会存在
  jQuery = window.jQuery;

  if (jQuery && jQuery.fn.on) {
     // 如果引入了jQuery,就使用jQuery
     jqLite = jQuery;
   // ......
  } else {
     //如果没有引用jQuery库,就使用自带的JQLite。
     jqLite = JQLite;
  }

  // 把结果赋给angular.element上
  // 所以使用angular.element()就和使用jQuery一样
  angular.element = jqLite;

  // 已经绑定过,启动的时候再次调用就不会再绑定了
  bindJQueryFired = true;
}

publishExternalAPI

这个方法的作用是:绑定一些方法到 angular 对象上,然后初始化 Angular 的核心模块。

function publishExternalAPI(angular) {  
  extend(angular, {
  // 在angular上添加工具函数
  // copy/extend/merge/equals/element/forEach/noop/bind/noop
  // toJson/fromJson/isXXX/callbacks/lowercase/uppercase
  // 还有比较重要的bootstrap
  });

  // 此方法会把angular对象绑定到window上
  // 然后把一个函数绑定到angular的module属性上
  // 最后返回这个函数,这个函数是一个模块加载器
  // 主要作用是创建和获取模块
  // 这里的angularModule函数就是angular.module函数
  angularModule = setupModuleLoader(window);

  // 创建ngLocale模块,核心模块ng会用到
  try {
    angularModule('ngLocale');
  } catch (e) {
    angularModule('ngLocale', []).provider('$locale', $LocaleProvider);
  }

  // 创建ng模块
  angularModule('ng', ['ngLocale'], ['$provide',
    function ngModule($provide) {
      // $$sanitizeUri 在 $compile 之前引入
      // 因为 $compile 会用到 $$sanitizeUri
      $provide.provider({
        $$sanitizeUri: $$SanitizeUriProvider
      });
      // ng模块中,定义 $compile 服务
      $provide.provider('$compile', $CompileProvider).
        directive({
        // 在ng模块中,添加各种内置指令
        // a/input/textarea/form/script/select/style/option
        // ngXXX
        }).
        directive({
          ngInclude: ngIncludeFillContentDirective
        }).
        directive(ngAttributeAliasDirectives).
        directive(ngEventDirectives);
      $provide.provider({
      // 在ng模块中,添加各种内置服务
      // $document/$window/$location/$interval/$timeout/$q/$log
      // $http/$animate/$controller/$parse
      });
    }
  ]);
}

setupModuleLoader

publishExternalAPI 中用到以一个比较重要的方法 setupModuleLoader,该函数返回了一系列的API,用于Angular组织模块,注册指令、服务、控制器等,也就是常用的 angular.module

function setupModuleLoader(window) {

  // 检查obj.name,如果有就是getter操作,否则就是setter操作
  function ensure(obj, name, factory) {
    return obj[name] || (obj[name] = factory());
  }

  //设置window.angular等于一个空对象
  var angular = ensure(window, 'angular', Object);

  // 把angular.module设置成这个module函数,并返回这个函数。
  return ensure(angular, 'module', function() {
    var modules = {};

    return function module(name, requires, configFn) {
      // 模块名字检查:名字不能是hasOwnProperty,代码略

      // 如果有同名的模块已经创建过,就把以前的那个模块删除
      // 这里使用了闭包,只能通过angular.module访问私有变量modules
      if (requires && modules.hasOwnProperty(name)) {
        modules[name] = null;
      }

      return ensure(modules, name, function() {
        if (!requires) {
          // 报错,没有创建模块就调用
          // 这里也使用了闭包,如果定义的时候传入了requires
          // 使用模块的时候requires就存在
          // angular.module(name, [])定义
          // angular.module(name)使用
        }

        var invokeQueue = [];  // 调用队列
        var configBlocks = []; // 配置块
        var runBlocks = [];    // 运行块
        var config = invokeLater('$injector', 'invoke', 'push', configBlocks);

        //模块实例的方法,要返回的对象,开放了很多常用的API
        var moduleInstance = {
          _invokeQueue: invokeQueue,
          _configBlocks: configBlocks,
          _runBlocks: runBlocks,
          requires: requires,
          name: name,
          // 各种常用接口,都是使用调用invokeLater加入队列
          // provider/factory/service/value/animation
          // decorator/filter/controller/directive
          constant: invokeLater('$provide', 'constant', 'unshift'),
          config: config,
          run: function(block) {
            runBlocks.push(block);
            return this;
          }
        };

        // 调用angular.module方法传入了三个参数时,就会执行config方法
        // 上面在定义ng模块时,就会传入第三个参数
        if (configFn) {
          config(configFn);
        }

        return moduleInstance;

        // 将用户注册的各种服务、指令、过滤器等放到调用队列中
        function invokeLater(provider, method, insertMethod, queue) {
          if (!queue) queue = invokeQueue;
          return function() {
            queue[insertMethod || 'push']([provider, method, arguments]);
            return moduleInstance;
          };
        }
      });
    };
  });

}

这里要注意多次调用的 invokeLater 函数。通过 invokeLater 可以返回一个闭包,当调用 configprovider(即moduleInstance返回的那些api)时,实质上就是调用这个闭包,例如调用 provider

app.provider('myprovider',['$window',function($window){  
  //code
}]);

// 实际是('$provide','provider',['$window',function($window){}]) push进invokeQueue数组中

注意此处只是入队操作,并未执行。在后面执行时,实际上是通过 第一个参数调用第二个参数方法名,把第三个参数当变量传入,即:

args[0][args[1]].apply(args[0],args[2]);  

注意:

  • 上面提到 run 方法比较特殊,只把 block 放入到 runBlock 中,并没有放入 invokeQueue 中,后面加载模块时,会获取所有的运行块,然后调用 invoke 去执行。
  • 入队时一般都是使用 push 方法,只用 constantunshift,目的在于常量在配置、运行之前,能够在其他所有块中使用。

angularInit

文档加载完成后,执行 angularInit 方法,也就是文档加载完成后,angular才启动。这个方法的实际用途就是找到包含 ng-app 属性(4种)的节点,获取启动模块,然后调用 bootstrap 函数启动Angular。

// 支持四种前缀
var ngAttrPrefixes = ['ng-', 'data-ng-', 'ng:', 'x-ng-'];

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

  // 先看指定的element是否包含ng-app属性(4种)
  // 比后面的通过属性来查找的优先级高
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';

    if (!appElement && element.hasAttribute && element.hasAttribute(name)) {
      appElement = element;
      module = element.getAttribute(name);
    }
  });

  // 查找包含ng-app属性(4种)的节点
  // 通过该属性获取启动模块
  forEach(ngAttrPrefixes, function(prefix) {
    var name = prefix + 'app';
    var candidate;

    if (!appElement && (candidate = element.querySelector('[' + name.replace(':', '\\:') + ']'))) {
      appElement = candidate;
      module = candidate.getAttribute(name);
    }
  });

  // 如果找到了app节点(appElement)
  // 也就确定了启动模块(module)
  // 就可以启动应用了
  if (appElement) {
    // 这个应该是:是否严格依赖注入
    config.strictDi = getNgAttribute(appElement, "strict-di") !== null;
    bootstrap(appElement, module ? [module] : [], config);
  }
}

bootstrap

找到 ng-app 节点后,调用 bootstrap 函数就可以启动Angular了,这个方法实际调用的是 doBootstrap 来启动应用,相比来说只是多了一些对于调试的处理:

var doBootstrap = function() {  
  element = jqLite(element);

  if (element.injector()) {
    var tag = (element[0] === document) ? 'document' : startingTag(element);
    // 报错,已经从该元素启动过
  }

  // 从angularInit我们知道此时 modules 暂时是这样的: modules = ['myApp'];
  modules = modules || [];
  modules.unshift(['$provide', function($provide) {
    $provide.value('$rootElement', element);
  }]);

  if (config.debugInfoEnabled) {
    // 覆盖用户模块中debugInfoEnabled的设置
    modules.push(['$compileProvider', function($compileProvider) {
      $compileProvider.debugInfoEnabled(true);
    }]);
  }

  // modules中插入ng和$provide
  // 此时modules是['ng',['$provide',function(){}],'myApp']
  modules.unshift('ng');

  // 重要!!把angular应用需要初始化的模块数组传进去后
  // 进行加载,并创建一个注册器实例对象,最后返回这个实例
  var injector = createInjector(modules, config.strictDi);

  // 调用注册器实例对象的invoke方法,实际就是执行bootstrapApply函数
  // bootstrapApply函数从$rootElement开始,绑定$rootScope,递归编译
  injector.invoke(['$rootScope', '$rootElement', '$compile', '$injector',
     function bootstrapApply(scope, element, compile, injector) {
      scope.$apply(function() {
        element.data('$injector', injector);
        compile(element)(scope);
      });
    }]
  );
  return injector;
};

调用 module 定义模块,调用 servicecontroller 等都是注册,并没有生成实例,只有调用 invoke 之后才是执行该方法,生成实例。上面启动过程调用了 $compile 编译整个文档,绑定 scope,关于编译的这部分以后再讨论。

createInjector

这个方法是编译之前调用的,是比较重要的一个函数,是 Angular 依赖注入的核心。下面来分析一下这个函数:

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(serviceName, caller) {
            if (angular.isString(caller)) {
              path.push(caller);
            }
            throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
          })),
      instanceCache = {},
      instanceInjector = (instanceCache.$injector =
          createInternalInjector(instanceCache, function(serviceName, caller) {
            var provider = providerInjector.get(serviceName + providerSuffix, caller);
            return instanceInjector.invoke(provider.$get, provider, undefined, serviceName);
          }));


  forEach(loadModules(modulesToLoad), function(fn) { instanceInjector.invoke(fn || noop); });

  return instanceInjector;
}

这个函数里主要是四个变量: providerCacheproviderInjectorinstanceCacheinstancheInjector

providerCache 初始化只有一个对象,紧接着调用 createInternalInjector 方法返回一个若干重量级api(annotateget等)赋值给 providerCache.$injector 以及 provoderInjector。结果就变成这样了:

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

// 返回的也是这个
providerCache.$injector = providerInjector  
= instanceCache.$injector = instanceInjector = {
  get:getService,
  annotate:annotate,
  instantiate:instantiate,
  invoke:invoke,
  has:has
}

可以说 moduleinjector 完成了 Angular 依赖注入的任务,这两者的关系简单表示如下:

module and injector

上面的代码用到了两个重要的方法 loadModulescreateInternalInjector

loadModules

这个方法用于加载模块,即返回需要运行的块,然后调用 invoke 方法执行生成实例,之前提到模块都是放入 invokeQueue 中,只是注册,并没有调用执行:

function loadModules(modulesToLoad) {  
    var runBlocks = [], moduleFn;
    // modulesToLoad长这样:['ng',['$provider',function(){}], 'myApp']
    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]);

          // 之前提到过,第一个参数调用名为第二个参数的方法,传入第三个参数
          provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
        }
      }

      try {
        if (isString(module)) {
          moduleFn = angularModule(module);
          //迭代,把所有依赖模块的runBlocks都取出
          runBlocks = runBlocks.concat(loadModules(moduleFn.requires)).concat(moduleFn._runBlocks);
          runInvokeQueue(moduleFn._invokeQueue);
          runInvokeQueue(moduleFn._configBlocks);
        } 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];
        }
        // 有错啦
      }
    });
    // 返回需要运行的块
    return runBlocks;
  }

createInternalInjector

这个函数是 Angular 的核心之一,因为这个函数才能有如此优雅的依赖注入:

  function createInternalInjector(cache, factory) {
    return {
      // 通过service对应的工厂函数还有本地依赖初始化service
      invoke: invoke,
      // 调用invoke实例化service
      instantiate: instantiate,
      // 通过名称和本地依赖,返回对应的service
      get: getService,
      // 返回一个数组,解析依赖的service的名称
      annotate: createInjector.$$annotate,
      // 依赖的service是否存在
      has: function(name) {
        return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
      }
    };
  }

这里再说一说获取依赖的 annotate

var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;  
var FN_ARG_SPLIT = /,/;  
var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/;  
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

// 去除注释
fnText = fn.toString().replace(STRIP_COMMENTS, '');  
// 获取参数列表
argDecl = fnText.match(FN_ARGS);  
// 逗号分隔获取所有依赖
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {  
  arg.replace(FN_ARG, function(all, underscore, name) {
      $inject.push(name);
  });
});
// 返回$inject

对于显示注入的情况比较简单,直接通过名字就可以获取依赖。以上代码主要处理的是隐式注入:首先去除注释,然后获取 function 括号里面的参数,然后用逗号分隔获取各个依赖,最后返回所有依赖。

再来看一看 invoke 的核心代码:

var args = [],  
    // 使用前面的annotate函数获取所有依赖
    // createInjector.$$annotate = annotate
    $inject = createInjector.$$annotate(fn, strictDi, serviceName);

for (i = 0, length = $inject.length; i < length; i++) {  
   key = $inject[i];
   if (typeof key !== 'string') {
      // 报错啦
   }
   // 如果是本地变量从locals取出即可
   // 如果是依赖的service,获取之
   args.push(
     locals && locals.hasOwnProperty(key)
     ? locals[key]
     : getService(key, serviceName)
   );
}
if (isArray(fn)) {  
  fn = fn[length];
}
// 调用方法,即实例化
return fn.apply(self, args);  

获取到依赖之后就是处理要执行函数的参数,如果是本地变量从 locals 中获取即可,否则调用 getService 获得实例,然后将参数注入函数执行即可。

getService 先从 cache 中获取 service 实例,如果缓存中没有就执行 service 的工厂方法生成实例,立即缓存该实例,然后返回该实例,下次有模块需要该 service 实例时直接从 cache 中获取即可。

有了注入器实例,再回到 bootstrap 函数,下一步就可以从 rootElement(ng-app元素所在节点)开始,注入 rootScope,编译整个文档了。

总结

启动流程用简单的图示表示如下: angular启动流程

Author image
关于 superlin
Beijing, CN 主页
The reason why a great man is great is that he resolves to be a great man.
默认颜色 边栏居左 边栏居右