AngularJS 确实是一个比较强大的框架,如何深入理解它然后写出比较漂亮的组件确实是一门学问,12bet,虽然目前实验室的项目中有使用它,总觉得不够完美,12bet,今日决定读一读源码。
这里我选用了最新的1.4.0的源码,12bet,作为第一篇就先来看看 AngularJS 是怎么启动的吧。
初始化
AngularJS 加载之后,就会有一段立即执行的初始化代码,请看第28121行之后:
// 绑定jQuery,后面的jqLite(document).ready用到
// 启动的时候会再次绑定
bindJQuery();
publishExternalAPI(angular);
jqLite(document).ready(function() {
angularInit(document, bootstrap);
});
初始化就只用三个函数,下面分别来看一看:
bindJQuery
这个方法的作用是:12博体育,检查主页面是否引用了 jQuery
,没有的话,12bet,就用Angular自带的 JQLite
,12博体育,否则使用引用的 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
可以返回一个闭包,当调用 config
, provider
(即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
方法,只用constant
是unshift
,目的在于常量在配置、运行之前,能够在其他所有块中使用。
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
定义模块,调用 service
、 controller
等都是注册,并没有生成实例,只有调用 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;
}
这个函数里主要是四个变量: providerCache
, providerInjector
, instanceCache
, instancheInjector
providerCache
初始化只有一个对象,紧接着调用 createInternalInjector
方法返回一个若干重量级api(annotate
,get
等)赋值给 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
}
可以说 module
和 injector
完成了 Angular 依赖注入的任务,这两者的关系简单表示如下:
上面的代码用到了两个重要的方法 loadModules
和 createInternalInjector
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 = /^functions*[^(]*(s*([^)]*))/m;
var FN_ARG_SPLIT = /,/;
var FN_ARG = /^s*(_?)(S+?)1s*$/;
var STRIP_COMMENTS = /((//.*$)|(/*[sS]*?*/))/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
,编译整个文档了。
总结
启动流程用简单的图示表示如下: