一个对象通常有三种方式可以获得对其依赖的控制权:
- 在内部创建依赖
- 通过全局变量进行引用
- 在需要的地方通过参数进行传递
依赖注入是通过第三种方式实现的。其余两种方式会带来各种问题,例如污染全局作用域,使隔离变得异常困难等。依赖注入是一种设计模式,它可以去除对依赖关系的硬编码,从而可以在运行时改变甚至移除依赖关系。
从功能上看,依赖注入会事先自动查找依赖关系,并将注入目标告知被依赖的资源,这样就可以在目标需要时立即将资源注入进去。
在编写依赖于其他对象或库的组件时,我们需要描述组件之间的依赖关系。在运行期,注入器会创建依赖的实例,并负责将它传递给依赖的消费者。
基于以上原因,AngularJS使用$injetor
(注入器服务)来管理依赖关系的查询和实例化。事实上,$injetor
负责实例化AngularJS中所有的组件,包括应用的模块、指令和控制器等。
在运行时,任何模块启动时$injetor
都会负责实例化,并将其需要的所有依赖传递进去。
angular.module('myApp', [])
.factory('greeter', function() {
return {
greet: function(msg) {alert(msg);}
}
})
.controller('MyController', function($scope, greeter) {
$scope.sayHello = function() {
greeter.greet("Hello!");
};
});
当AngularJS实例化这个模块时,会查找greeter
并自然而然地把对它的引用传递进去。
<div ng-app="myApp">
<div ng-controller="MyController">
<button ng-click="sayHello()">Hello</button>
</div>
</div>
而在内部,AngularJS的处理过程是下面这样的:
// 使用注入器加载应用
var injector = angular.injector(['ng', 'myApp']);
// 通过注入器加载$controller服务:var $controller = injector.get('$controller');
var scope = injector.get('$rootScope').$new();
// 加载控制器并传入一个作用域,同AngularJS在运行时做的一样
var MyController = $controller('MyController', {$scope: scope})
上面的代码中并没有说明是如何找到greeter
的,但是它的确能正常工作,因为$injector
会负责为我们查找并加载它。
AngularJS通过annotate
函数,在实例化时从传入的函数中把参数列表提取出来。
> injector.annotate(function($q, greeter) {})
["$q", "greeter"]
在任何一个AngularJS的应用中,都有$injector
在进行工作,无论我们知道与否。当编写控制器时,如果没有使用[]
标记或进行显式的声明,$injector
就会尝试通过参数名推断依赖关系。
推断式注入声明
如果没有明确的声明,AngularJS会假定参数名称就是依赖的名称。因此,它会在内部调用函数对象的toString()
方法,分析并提取出函数参数列表,然后通过$injector
将这些参数注入进对象实例。注入的过程如下:
injector.invoke(function($http, greeter) {});
这个过程只适用于未经过压缩和混淆的代码,因为AngularJS需要原始未经压缩的参数列表来进行解析。
有了这个根据参数名称进行推断的过程,参数顺序就没有什么重要的意义了,因为AngularJS会帮助我们把属性以正确的顺序注入进去。
显式注入声明
AngularJS提供了显式的方法来明确定义一个函数在被调用时需要用到的依赖关系。通过这种方法声明依赖,即使在源代码被压缩、参数名称发生改变的情况下依然能够正常工作。
可以通过$inject
属性来实现显式注入声明的功能。函数对象的$inject
属性是一个数组,数组元素的类型是字符串,它们的值就是需要被注入的服务的名称。
var aControllerFactory=function aController($scope,greeter) {
console.log("LOADED controller", greeter);
// ……控制器
};
aControllerFactory.$inject = ['$scope', 'greeter']; // Greeter服务
console.log("greeter service");
// 我们应用的控制器
angular.module('myApp', [])
.controller('MyController', aControllerFactory)
.factory('greeter', greeterService);
// 获取注入器并创建一个新的作用域
var injector = angular.injector(['ng', 'myApp']),
controller = injector.get('$controller'),
rootScope = injector.get('$rootScope'),
newScope = rootScope.$new();
// 调用控制器
controller('MyController', {$scope: newScope});
对于这种声明方式来讲,参数顺序是非常重要的,因为$inject
数组元素的顺序必须和注入参数的顺序一一对应。这种声明方式可以在压缩后的代码中运行,因为声明的相关信息已经和函数本身绑定在一起了。
行内注入声明
行内注入声明同前面提到的通过$inject
属性进行注入声明的原理是完全一样的,但允许我们在函数定义时从行内将参数传入。此外,它可以避免在定义过程中使用临时变量。
在定义一个AngularJS的对象时,行内声明的方式允许我们直接传入一个参数数组而不是一个函数。数组的元素是字符串,它们代表的是可以被注入到对象中的依赖的名字,最后一个参数就是依赖注入的目标函数对象本身。
angular.module('myApp')
.controller('MyController',['$scope','greeter',function($scope,greeter) {
}]);
由于需要处理的是一个字符串组成的列表,行内注入声明也可以在压缩后的代码中正常运行。通常通过括号和声明数组的[]
符号来使用它。
$injector API
annotate()
annotate()
方法的返回值是一个由服务名称组成的数组,这些服务会在实例化时被注入到目标函数中。annotate()
方法可以帮助$injector
判断哪些服务会在函数被调用时注入进去。
annotate()
方法可以接受一个参数:fn
(函数或数组),参数fn
可以是一个函数,也可以是一个数组。annotate()
方法返回一个数组,数组元素的值是在调用时被注入到目标函数中的服务的名称。
var injector = angular.injector(['ng', 'myApp']);
injector.annotate(function($q, greeter) {});
// ['$q', 'greeter']
get()
get()
方法返回一个服务的实例,可以接受一个参数:name
(字符串),参数name
是想要获取的实例的名称。get()
根据名称返回服务的一个实例。
has()
has()
方法返回一个布尔值,在$injector
能够从自己的注册列表中找到对应的服务时返回true
,否则返回false
。它能接受一个参数:name
(字符串),参数name
是我们想在注入器的注册列表中查询的服务名称。
instantiate()
instantiate()
方法可以创建某个JS类型的实例。它会通过new
操作符调用构造函数,并将所有参数都传递给构造函数。它可以接受两个参数。
- Type(函数):构造函数。
- locals(对象,可选):这是一个可选的参数,提供了另一种传递参数的方式。
instantiate()
方法返回Type
的一个新实例。
invoke()
invoke()
方法会调用方法并从$injector
中添加方法参数。
invoke()
方法接受三个参数。
- fn(function):这个函数就是要调用的函数。这个函数的参数由函数声明设置。
- self (object-可选):
self
参数允许我们设置调用方法的this
参数。 - locals (object-可选):这个可选参数提供另一种方式在函数被调用时传递参数名给该函数。
invoke()
方法返回fn
函数返回的值。