5 依赖注入

依赖注入

简介

依赖注入(DI)是一种让代码管理其依赖关系的设计模式。

对象或函数可以通过三种方式获得所依赖的对象(简称依赖):

  1. 创建依赖,通常是通过 new 操作符

  2. 查找依赖,在一个全局的注册表中查阅它

  3. 传入依赖,需要此依赖的地方等待被依赖对象注入进来

前两种方式:创建或是查找依赖都不是那么理想,因为它们都将依赖写死在对象或函数里了。问题在于,想要修改这两种方式获得依赖对象的逻辑是很困难的。尤其是在测试的时候,会遇到很多问题,因为测试时常常需要我们提供所依赖对象的替身(MOCK)。

第三种方式是最理想的,因为它免除了客户代码里定位相应的依赖这个负担,反过来,依赖总是能够很简单地被注入到需要它的组件中。

为了分离“创建依赖”的职责,每个 Angular 应用都有一个 injector对象。这个 injector 是一个服务定位器,负责创建和查找依赖。

下面是一个利用 injector 服务例子:

// Provide the wiring information in a module
angular.module('myModule', []).

// 下面是教 injector 如何构建一个 'greeter' 依赖
// 注意 greeter 本身依赖于 '$window'
factory('greeter', function($window) {
// 这是一个 factory 函数,负责创建 'greeter' 服务
return {
greet: function(text) {
$window.alert(text);
}
};
})
.controller('MyController',
function($scope, greeter) {
$scope.sayHello = function() {
greeter.greet("Hello!");
};
});
<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})

实现原理

对于一个DI容器来说,必须具备3个要素:服务的注册、依赖关系的声明、对象的获取。在angular中,module和$provide相当于是服务的注册;injector用来获取对象(angular会自动完成依赖的注入);依赖关系的声明在angular中有3种方式。

一般来说,服务在模块内部,当我们需要某个服务的时候,是先把模块实例化,然后再调用模块的方法。但Angular模块和我们通常理解的模块不一样,Angular模块只保留服务的声明,服务的实例化是由服务注入器完成的,实例化之后服务就留在了服务注入器中,和模块没有关系了,这就是为什么我们使用的服务全部来自注入器的原因。

在了解服务注入器$injector前,还需要了解另一个概念,就是服务提供商,在Angular中称为Provider,几乎所有的服务(除了$injector)都是由服务提供商供应。无论是服务还是服务提供商,他们在Angular中都是唯一的,服务和服务提供商是一个一对一的关系。也正是因为这个关系,我们在使用模块service()、value()等方法时,感觉我们定义的好像只是服务,但其实Angular背后为这些服务都创建了一一对应的服务提供商。注入器的机制就是当我们需要某个服务的时候,首先根据服务名找到对应的服务提供商,然后由服务提供商创建对应的服务并返回。

所以这就是整个过程(依赖注入的过程):

  1. 模块定义服务、服务提供商;
  2. 注入器根据模块依赖关系加载模块,实例化所有服务提供商;
  3. 应用需要服务,注入器根据服务名寻找服务提供商,服务提供商实例化服务。

以上只是理论,现在从代码层面来看Angular是如何实现的。

每个angular模块内置有三个数组:

  1. invokeQueue保存如何注入服务提供商和值的信息;
  2. configBlocks保存模块的配置信息;
  3. runBlocks保存这个模块的执行信息。

模块被使用的时候,注入器根据invokeQueue中的信息,实例化服务提供商;根据configBlocks中的信息对服务提供商做一些额外的处理;根据runBlocks中提供的信息,调用前面的服务提供商提供的服务执行模块需要完成的工作。

angular模块提供了很多方法来填充这三个数组,比如config()、run()等。三个数组的元素也是数组.

调用队列 – invokeQueue

数组元素为[‘provider’, ‘method’, arguments]。

举例– 添加一个Controller:

angular.module('ngAppDemo',[])
.controller('ngAppDemoController',function($scope) {
$scope.a= 1;
$scope.b = 2;
});

这段代码等于:

invokeQueue.push(['$controllerProvider','register', ['ngAppDemoController', function(){}]]);

注入器根据这个信息,就会调用$controllerProvider的register方法注册一个ngAppDemoController。

配置队列 – configBlocks

元素格式为[‘$injector’, ‘invoke’, arguments]。

运行队列 – runBlocks

元素要求是方法,或者是数组,数组最后一个元素是方法。

在Angular中,服务可能是对象、方法、或者一个常量值。服务由服务提供商创建,而服务提供商由注入器统一管理。当我们需要某个服务的时候,注入器负责根据服务名寻找相应的服务提供商,然后由服务提供商的$get()生产工厂创建服务实例。因此,服务提供商必须有一个$get()方法,这个方法就是服务创建单例工厂。

背后原理:注入器中的Providers和Services各自通过一个Map对象保存在缓存(分别对应providerCache和instanceCache)中,只不过Providers的key是serviceName + “Provider”,而Services的key是serviceName。

注入器$injector

每调用一次angular.boostrap()方法会创建一个新的Angular应用和一个新的服务注入器,因此,每个应用都对应一个服务注入器,彼此互不冲突。

每个AngularJS应用都有一个唯一的注入器用来处理依赖的创建。注入器提供一个通过名字查找对象实例的方法。它将所有对象缓存在内部,所以如果重复调用同一名称的对象,每次调用都会得到同一个实例。如果调用的对象不存在,那么注入器就会让实例工厂(instance factory)创建一个新的实例。

三个固定模块

每个使用bootstrap(element, modules, config)生成的应用,注入器中有三个固定的模块:

  • 第一个模块是”ng”。
  • 第二个模块是['$provider',fn],它的作用是把根元素element作为变量保存在$provider中。
  • 第三个模块是['$compileProvider',fn],它的作用是根据config.debugInfoEnabled调用 $conpileProvider.debugInfoEnabled(true)

注入过程

注入器由angular.injector(modulesToLoad, isStrictDi)方法创建,在angular中其实为createInjector方法。参数modulesToLoad是数组,元素格式为以下之一:

  • ‘module’,模块的名称。
  • [‘service1’, ‘service2’, fn]。fn,方法的返回值必须仍然是方法。

方法angular.injector()的执行过程:

  1. 遍历modulesToLoad,根据moduleName寻找或生成相应的模块。
  2. 调用模块invokeQueue中的所有方法,这一步的目的是创建必需的服务。
  3. 调用模块configBlocks中的所有方法,目的是用已有的服务配置这个模块。
  4. 调用注入器的invoke()方法执行模块runBlocks的所有方法。
  5. 返回服务注入器实例。

不是所有模块都是对象,如果模块本身是方法或者是数组(最后一个元素必须是方法),则运行这个方法、或数组的最后一个方法,相当于直接进入了第四步。

如果加载了多个模块,那么通过返回的injector可以获取到多个模块下的服务。下面例子中如果只加载了myMoudle,那么得到的injector就不能访问herMoudle下的服务。这里特别需要注意下:angular.injector()可以调用多次,每次都返回新建的injector对象。

var injector1 = angular.injector(["myModule","herModule"]);
var injector2 = angular.injector(["myModule","herModule"]);
alert(injector1 == injector2);//false

注入器方法

在providerCache中和instanceCache中分别内置有一个$injector对象,分别负责给模块注入服务提供商和为方法注入服务。一般我们只谈论instanceCache中的$injector对象,因为providerCache和它的$injector是私有的,只在Angular内部代码使用。

比如,执行模块调用队列、配置队列中的方法时注入的是服务提供商,而当调用运行队列中的方法时,注入的是服务。

  • $injector.has(name)

从注册的列表查找对应的服务,如果找到返回true,否则返回false
* $injector.get(name, [caller])

返回指定名称的服务实例,获取到服务的实例对象后,就可以直接调用服务中的属性和方法。
参数caller也是字符串,表示调用这个服务的是哪个方法,用于错误信息提示,没有处理逻辑。
* $injector.invoke(fn, [self], [locals], [serviceName])

执行方法fn。locals是可选参数,是对象,表示局部变量。self是fn中的this。
最后一个参数serviceName是可选参数,表示在哪个服务中调用了fn方法,用于错误信息显示,没有处理逻辑。
* $injector.instantiate(Type,[ locals], [serviceName])

  • 如果参数Type为方法,根据Type的prototype创建一个实例(通过Object.create方法创建),如果Type是数组,使用最后一个元素的prototype。
  • 参数Locals是当Type方法的参数出现在locals对象中的时候,取locals[arg]的值重新作为Type的参数。如果locals中没有,则等价于调用get(arg,serviceName)获取service作为新的参数。
  • 参数serviceName表示在哪个服务中实例化了Type,用于错误信息显示,没有处理逻辑。

实例化过程可以简单概括为
Type.apply(Object.create(Type.prototype),locals[argName]|| get(argName, serviceName))。
注意:实例化出来的不一定是对象,也可能是方法。
* $injector.annotate(fn,[strictDi],[name])

返回数组,数组的元素是fn方法需要注入的依赖服务。
在严格模式下,方法的依赖注入必须使用显示的注解加入,也就是说通过fn.$injector能够获取这个方法的依赖注入。
参数name是可选的,用于错误显示,没有处理逻辑。
方法annotate()也可以接受数组,数组的最后一个参数一定是fn,前面的元素则是依赖。

服务实例化

所有服务都由Provider管理,除了常量值,其它一般都是还没有创建出来的。了解了注入器的所有方法,也应该可以看出,只有两个方法:get()和invoke()真实的调用了服务。其实,服务的实例化实际是在注入器的get()方法中完成的,而invoke()只是在需要的时候调用了get()。

服务提供者$provider

Provider即服务提供商,必须有一个$get()方法,$get()的返回值是Provider在注入器中实际的服务。

Provider方法

注入器的providerCache中内置有一个$provider对象,这是注入器的默认服务提供商,$provider有六个固定的方法。这几个方法的作用主要是为注入器添加其他服务提供商。

注意:

  • 以下所有方法的name参数不需要以“Provider”结尾,因为provider()方法会默认把这个后缀加上。
  • 以下任何一个方法不做同名判断,因此,如果出现同名,后者将覆盖前者。

1. $provider.provide(name, provider)

参数provider可以是方法或数组,也可以是对象。

  • 如果是方法,则是provider的构造函数。调用注入器的instantiate()方法,生成一个provider实例,并以name为key保存在注入器的providerCache中。
  • 如果是数组,最后一个必须是provider的构造函数,前面的就是构造函数的参数名。之后的原理和provider是方法的情形相同。
  • 如果是对象,说明这个provider已经被实例化了,只需有$get()方法即可。

2. $provider.factory(name, factoryFn, enforce)

使用$provider.provide()一般需要定义一个Provider类,如果不想定义Provider类,而是直接定义服务工厂,就可以使用这个方法。

背后原理:首先生成一个匿名对象,这个对象的$get属性就是factoryFn(enforce为false的情况下),然后把这个匿名对象作为$provider.provide()方法的第二个参数。所以,factoryFn其实依然是绑定在一个provider上的。

3. $provider.service(name, constructor)

调用injector.instantiate()方法,利用参数constructor生成service实例,参数name是这个service的名称。

众所周知,service由provider提供,那这个方法是怎么回事?原理:Angular首先根据constructor生成一个factoryFn,然后调用$provider.factory(name, factoryFn)。所以其实还是生成了一个provider。举例:
$provider.service('filter', constructor)
等于创建了一个filter服务实例,并且在providerCache中保存了一个名称为“filterProvider”的服务提供商。

4. $provider.value(name, value)

这个方法实际调用injector.factory(name,valueFn(value), false)方法实现。所以其实等于创建一个只提供值的服务提供商。

5. $provider.constant(name, value)

这个方法直接在providerCache中添加一个属性实现。

6. $provider.decorate(serviceName, decorFn)

修改旧的服务,改为执行decorFn方法,并把servcieName原来的服务作为一个参数,参数名为$delegate。等价于一个静态代理。

背后原理:首先根据seviceName找到Provider,然后修改provider的$get属性。

angular中三种声明依赖的方式

在Angular中,依赖注入可谓无孔不入。通常在两种场景(函数)下会使用到依赖注入:

  1. 工厂方法定义的组件(components):如directive,factory,filter,provider,controller等。这些工厂函数需要注册到某个模块上。controller比较特殊,它虽然也是一种组件,但是特别之处是它与某个DOM元素关联,因此可以注入$scope service,而其他组件只能注入$rootScope service。

  2. 模块提供的run/config方法。

我们称定义组件的工厂方法和run/config方法是可注入的。

Angular支持三种定义依赖注入的方式:inference、annotation、inline方式。

// 创建myModule模块、注册服务
var myModule = angular.module('myModule', []);
myModule.service('myService', function() {
this.my = 0;
});
// 获取injector
var injector = angular.injector(["myModule"]);
// 第一种inference
injector.invoke(function(myService){alert(myService.my);});
// 第二种annotation
function explicit(serviceA) {alert(serviceA.my);};
explicit.$inject = ['myService'];
injector.invoke(explicit);
// 第三种inline
injector.invoke(['myService', function(serviceA){alert(serviceA.my);}]);

其中annotation和inline方式,对于函数参数名称没有要求,是推荐的做法;inference方式强制要求参数名称和服务名称一致,如果JS代码经过压缩或者混淆,那么功能会出问题,不建议使用这种方式。

上面的例子通过请求依赖的方式解决了硬编码的问题,但是同样也意味着注入器需要在应用中传递,这违反了迪米特法则。所以我们一般是在控制器中注入依赖。

inference推断式/隐式

最简单的处理依赖的方法,就是假设函数的参数名就是依赖的名字。推断式注入需要保证参数名称与服务名称相同。如果代码要经过压缩等操作,就会导致注入失败。

app.controller("myCtrl1", function($scope,hello1,hello2){
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
});

annotation $inject 标记

标记式注入需要设置一个依赖数组,数组内是依赖的服务名字,在函数参数中,可以随意设置参数名称,但是必须保证顺序的一致性。

var myCtrl2 = function($scope,hello1,hello2){
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
}
myCtrl2.$injector = ['$scope','hello1','hello2'];
app.controller("myCtrl2", myCtrl2);

inline数组/内联标记

内联式注入直接传入两个参数,一个是名字,另一个是一个数组。这个数组的最后一个参数是真正的方法体,其他的都是依赖的目标,但是要保证与方法体的参数顺序一致(与标记注入一样)。推荐使用。

app.controller("myCtrl3",['$scope','hello1','hello2',function($scope,hello1,hello2){
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
}]);

完整示例

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<script src="http://apps.bdimg.com/libs/angular.js/1.2.16/angular.min.js"></script>
</head>
<body ng-app="myApp">
<div ng-controller="myCtrl1">
<input type="button" ng-click="hello()" value="ctrl1"></input>
</div>
<div ng-controller="myCtrl2">
<input type="button" ng-click="hello()" value="ctrl2"></input>
</div>
<div ng-controller="myCtrl3">
<input type="button" ng-click="hello()" value="ctrl3"></input>
</div>
<script type="text/javascript">
var app = angular.module("myApp",[]);
app.factory("hello1",function(){
return {
hello:function(){
console.log("hello1 service");
}
}
});
app.factory("hello2",function(){
return {
hello:function(){
console.log("hello2 service");
}
}
});
var $injector = angular.injector();
console.log(angular.equals($injector.get('$injector'),$injector));//true
console.log(angular.equals($injector.invoke(function($injector) {return $injector;}),$injector));//true
//inferred
// $injector.invoke(function(serviceA){});
app.controller("myCtrl1", function($scope,hello1,hello2){
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
});
//annotated
// function explicit(serviceA) {};
// explicit.$inject = ['serviceA'];
// $injector.invoke(explicit);
var myCtrl2 = function($scope,hello1,hello2){
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
}
myCtrl2.$injector = ['hello1','hello2'];
app.controller("myCtrl2", myCtrl2);
//inline
app.controller("myCtrl3",['$scope','hello1','hello2',function($scope,hello1,hello2){
// app.controller("myCtrl3",['$scope','hello1','hello2',function(a,b,c){
// a.hello = function(){
// b.hello();
// c.hello();
// }
$scope.hello = function(){
hello1.hello();
hello2.hello();
}
}]);
console.log($injector.annotate(myCtrl2));//["$scope","hello1","hello2"]
</script>
</body>
</html>

config/run方法

define(['angular'], function(angular){
return angular.module('myCat', [])
.provider('hello', function() {
var firstName = '';
this.makeName = function(first){
firstName = first;
}
this.$get = function() {
return function(name) {
alert("Hello, " + firstName + ' ' + name);
};
};
})
.config(function(helloProvider){
helloProvider.makeName('Circle');
})
.controller('catCtrl', [
'$scope',
'$q',
'hello',
function($scope, $q, hello){
$scope.catName = "";
$scope.sayHello = function(){
hello('Jiang');//output 'Hello, Circle Jiang'
};
}]
);
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值