一、准备
angular的源码一份,我这里使用的是v1.4.7。源码的获取,请参考我另一篇博文:angular源码分析:angular源代码的获取与编译环境安装
二、什么是依赖注入
据我所知,依赖注入的概念最早使用时在java编程中。依赖注入和控制反转差不多是一个概念,是编程中一种重要的解耦手段。依赖注入不是目的,它是一系列工具和手段,最终的目的是帮助我们开发出松散耦合、可维护、可测试的代码和程序。这条原则的做法是大家熟知的面向接口,或者说是面向抽象编程。具体含义是:当某个角色(可能是一个对象实例,调用者)需要另一个角色(另一个对象实例,被调用者)的协助时,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在angular里,创建被调用者的工作不再由调用者来完成,因此称为控制反转;创建被调用者实例的工作通常由angular来完成,然后注入调用者,因此也称为依赖注入。
写了这么多,举个具体的例子:
angular.module('myService',['ng']);
angular
.module('myService')
.factory('currentTime',function($window){
return function(){
var now = $window.Date();
return now;
};
});
在上面这段代码中,我首先创建了一个"myService"的模块,让后在这个模块上创建了一个叫"currentTime"的服务。
我编写的这个服务是依赖于另一个服务"$window"。如果我的服务"currentTime",在其他地方被另外的代码依赖,那么angular的框架就会自动去查找是否存在$window这样的服务,如果存在,就会将其传入到我代码中定义的工厂方法中,来实例化一个currentTime,并且将currentTime记录下来,下次需要就不再实例化,而是直接给与;如果不存在,就会先去实例化$window这个服务。
这里需要注意的在angular中,服务都是单实例形式存在(可以利用这点来完成组件间的通信),控制器(controller)是可以多实例的。
这么做好:
1.松散耦合。编写组件的标准变成:有且只有依赖于被 “依赖注入”的对象,不允许依赖于其他。这样,一个对象与外界的“扇入”就被限制在了所依赖的对象范围内,是一种可控的状态,“扇出”同样只能在被依赖的时候才会建立。
2.可维护。由于上面的性质,决定了依赖于某对象的对象可以不用管依赖住对象的内部实现。依赖对象对象外的只有接口和实现的功能,当需求发生变化的时候,我就需要修改对象的依赖模块就行了。
3.可测试。想想在对某个模块做单元测试,在有依赖注入的情况下,只需要构建理想的被依赖模块,注入到这个要测试的模块中,然后检查输出结构就行了。
三、js中依赖注入的原理
1.js中的函数有一个默认的方法toString()
可以将函数的作为字符串输出。举例:
function test(params){
//在控制台输出
console.log('this is a test');
}
var out = test.toString();
在上面的代码中,out的内容将是function test(params){
//在控制台输出
console.log('this is a test');
}
2.可见,通过toString可以得到函数的具体实现,当让也可得到函数所需要的参数
3.那么我们在调用函数前就可以根据函数中的参数来实现将函数中的参数对象构建出来,传递给它。这样就可以完成依赖注入。
4.但是问题来了。我们知道在js的发布上线的时候,我们为了减小代码的体积,减轻网络传输的压力,会代码进行压缩处理,压缩的过程中,会删除注释和多余的空格,更高级的压缩会替换代码中的变量,将代码中的变量都替换成最短的变量名。这样函数依赖的对象名也就被破坏掉了。
5.解决方法:
a.换种方式写,将依赖用字符串标出,显试的指明依赖对象:之前的currentTime这样写:
angular.module('myService',['ng']);
angular
.module('myService')
.factory('currentTime',['$window',function($window){
return function(){
var now = $window.Date();
return now;
};
}]);
由于代码中的'$window'是一个字符串常量,在压缩中是不会被替换的。
b.找angular专用的代码压缩工具来压缩代码
四、angular中是如何实现依赖注入的
依赖注入的功能主要由源代码包中./src/auto/injector.js完成。
先上代码,直接在代码中注释
//处理匿名函数,有参数的函数返回‘function(参数)’
//没有参数的函数,返回‘fn’
//这一段有大量的正则表达式,请自行查阅,有机会我会写一篇《高效的正则表达式》的博文。
function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
args = fnText.match(FN_ARGS);
if (args) {
return 'function(' + (args[1] || '').replace(/[s
]+/, ' ') + ')';
}
return 'fn';
}
//这个函数的名字叫“注释者”,功能却是返回函数的的参数列表
function annotate(fn, strictDi, name) {
var $inject,
fnText,
argDecl,
last;
if (typeof fn === 'function') { //函数使用传统的方式定义的,function(xx){xxx}
if (!($inject = fn.$inject)) {
$inject = [];
if (fn.length) {
if (strictDi) {
if (!isString(name) || !name) {
name = fn.name || anonFn(fn);
}
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
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);
});
});
}
fn.$inject = $inject;
}
} else if (isArray(fn)) { //函数是以数组的方式定义的,['xx',function(xx){...}]
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
}
下面看angular"内部注入器"的代码:
////////////////////////////////////
// 内部注入器
////////////////////////////////////
//这个函数是一个工厂方法,将生成一个“内部注入器”对象
function createInternalInjector(cache, factory) {
function getService(serviceName, caller) { //根据名字获取服务,如果服务不存在,尝试使用factory(serviceName, caller)创建服务
if (cache.hasOwnProperty(serviceName)) {
if (cache[serviceName] === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
serviceName + ' <- ' + path.join(' <- '));
}
return cache[serviceName];
} else {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
return cache[serviceName] = factory(serviceName, caller);
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
}
throw err;
} finally {
path.shift();
}
}
}
function invoke(fn, self, locals, serviceName) { //调用函数
if (typeof locals === 'string') {
serviceName = locals;
locals = null;
}
var args = [],
$inject = createInjector.$$annotate(fn, strictDi, serviceName), //前面讲过,这里取出的是函数的参数列表,而参数代表了函数依赖的对象
length, i,
key;
for (i = 0, length = $inject.length; i < length; i++) { //根据函数列表,构建函数所依赖的的服务(对象)
key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
args.push(
locals && locals.hasOwnProperty(key)
? locals[key]
: getService(key, serviceName)
);
}
if (isArray(fn)) {
fn = fn[length];
}
// http://jsperf.com/angularjs-invoke-apply-vs-switch
// #5388
return fn.apply(self, args); //调用函数,到此完全符合我讲的的依赖注入原理
}
//实例化,用户angular内部实例化controller、service、provider等
function instantiate(Type, locals, serviceName) {
// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
// Object creation: http://jsperf.com/create-constructor/2
var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
var returnedValue = invoke(Type, instance, locals, serviceName);
return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
}
//“内部注入器”对象
return {
invoke: invoke, //1.函数调用方法
instantiate: instantiate, //2.内部实例化方法
get: getService, //3."服务对象"获取方法
annotate: createInjector.$$annotate, //获取参数列表的方法
has: function(name) {
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
}
};
}
}
createInjector.$$annotate = annotate;
到此,可以理解angular的依赖注入式如何完成的。
上一期:angular源码分析:angular源代码的获取与编译环境安装
下一期:angular源码分析:angular中各种常用函数,比较省代码的各种小技巧