本文结合一些资料,谈谈AngularJS的依赖注入机制。主要参考资料有:
1. AngularJS官方文档:https://docs.angularjs.org/guide/di
2. Github文章:https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
3. 专著:Pro AngularJS
4. AngularJS 源代码
一、关于依赖注入
依赖注入式AngularJS的重要特性之一,有关概念和定义参考维基百科。依赖注入简化了Angular解析模块/组件之间依赖的过程。通常一个组件要获得它的依赖,有三种方式:
- 直接创建出依赖,如使用new操作符
- 能够查找到依赖,如引用全局变量
- 在需要的地方传入依赖
第三种的优势在于组件省去了定义/定位依赖的过程,也使得依赖的耦合度降低,可扩展性更强。依赖注入主要有两种形式(看
这里
):setter注入和constructor注入。前者的代表是Spring框架的setter方式,AngularJS则使用的是constructor注入。这里简单说一下二者的区别,setter注入顾名思义,首先使用一个无参的默认构造器构造对象,然后使用setter方法将依赖注入到新对象中。这种方式的一个缺陷是在编译时并不知道对象之间的依赖关系,依赖解析由某种框架在运行时完成。如果在配置依赖时有遗漏,则在运行时会报空引用错误。对于constructor注入,在构造对象时,将依赖的组件以参数形式传入构造器,传入组件所依赖的组件以相同的方式构造,依此类推,确保依赖链条最顶端的组件首先构造,一直到当前需要注入的对象构造完毕。该方式避免了依赖的遗漏。
二、AngularJS中的依赖注入
Angular的injector子系统负责创建组件,解析依赖,并将其按需提供给其他组件。每个Angular应用都有一个injector。Angular
在应用的启动阶段
(bootstrap,参见前面的文章)会创建一个injector:
var injector = angular.injector(['ng','myApp']);
injector方法的数组参数定义了可以提供依赖组件的模块,一般会提供模块'ng'和应用首先加载的模块'myApp'。查看源码发现,injector方法实际上实现为createInjector方法,由bootstrap方法调用。createInjector内部调用createInternalInjector方法返回真正的injector对象,返回结果如下:
return { invoke: invoke,instantiate: instantiate,get: getService,annotate: createInjector.$$annotate,has: function(name) { return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name); } };
其中前三个方法很重要,例如可以通过get方法获得一个需要注入的组件:
var comp = injector.get('component');
在Angular中,依赖注入可谓无孔不入。通常在两种场景(函数)下会使用到依赖注入:
- 工厂方法定义的组件(components):如directive,factory,filter,provider,controller等。这些工厂函数需要注册到某个模块上。controller比较特殊,它虽然也是一种组件,但是特别之处是它与某个DOM元素关联,因此可以注入$scope service,而其他组件只能注入$rootScope service。
- 模块提供的run/config方法。
Angular支持三种定义依赖注入的方式:
数组标注
:最常用且推荐的方式。例如:
<pre class="javascript" name="code">myApp.controller('smallCatCtrl',['$scope',function($scope){ $scope.sayCat = function(){ alert('I Love Circle!'); } }]);
var MyController = function($scope,myService) { // ... } MyController.$inject = ['$scope','myService']; myApp.controller('MyController',MyController);
myApp.controller('smallCatCtrl',function($scope){ $scope.sayCat = function(){ alert('I Love Circle!'); } });
另外,Angular支持使用严格注入模式:ng-strict-di,该声明与ngApp位于同一元素。严格模式下,不允许隐式注入,如果使用Angular会抛出异常。
要获得Angular应用的injector,只需注入$injector 即可,有意思的是它知道如何注入自己。通过使用$injector service的invoke方法,可以将组件注入到任何方法中,如:
var myFunction = function(hello) { hello('My Cat'); }; $injector.invoke(myFunction);
这段代码将hello service注入到myFunction函数作为其一个参数。我们定义组件(如controller,directive,filter,factory)所使用的工厂方法就是通过invoke函数注入依赖的。例如在Angular启动时,bootstrap方法会有如下逻辑:
injector.invoke(['$rootScope','$rootElement','$compile','$injector',function bootstrapApply(scope,element,compile,injector) { scope.$apply(function() { element.data('$injector',injector); compile(element)(scope); }); }] );
$rootScope等service被注入到bootstrapApply方法中,辅助完成启动过程。
injector针对每个可注入组件只创建一个实例(调用injector.instantiate方法),创建之后会将其缓存,以备后续访问。如图:(来源:https://docs.angularjs.org/guide/di)
三、Angular中的可注入组件
前面谈到了工厂方法以及config/run方法为可注入方法。下面稍微展开一点。首先从Angular中的
provider
说起。provider的概念比较抽象,简单来说,provider支持为某个组件(如factory,service,value等)在
加载时
提供一些配置。最终,每个provider会被注入到特定的组件使用。在Angular中,factory,service和value本质上都是一种provider,Angular会执行如下代码定义这些provider:
myApp.config(function($provide) { $provide.provider('sayHello',function() { this.$get = function() { return function(name) { alert("Hello,cat " + name); }; }; }); });
这里的config方法稍后再谈。$provide 是Angular内部提供的一个service,用来定义provider。$get方法返回了真正的service。为了简单,使用具体的provider类型进行定义也可以,这样还可省去get方法:
myApp.config(function($provide) { $provide.factory('sayCat',function() { return function(name) { alert("Hello,Cat" + name); }; }); });
这样就定义了一个factory service。如果把这套定义逻辑抽象出来,就成了Angular模块的一系列定义service的方法:
myMod.factory("sayHello",...); myMod.service("sayHello",...); myMod.value("Cat",...);
下面说说config/run方法。Angular在加载模块时经过两个阶段:config和run。传入config函数的方法会在当前模块加载时执行;传入run函数的方法会在所有模块加载结束后执行。因此,在config阶段即可配置多个provider,但是在config阶段,只有provider可以注入,因此自定义的service无法注入到config中。这也好理解,因为config阶段是对service进行配置的而不是使用service本身。在前面的代码中,$provide service本身就是一个provider(对于Angular应用来讲相当于一个'元provider'),在模块加载时会调用$provide的provider方法定义一个新的provider。再看一个具体的例子:
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,$defer,hello){ $scope.catName = ""; $scope.sayHello = function(){ hello('Jiang');//output 'Hello,Circle Jiang' }; }] ); });
在myCat模块中定义了一个名为hello的provider。该provider提供了一个makeName配置方法,用于设置firstName。$get方法返回一个函数用于打印一句话,这是该provider的核心功能(记住provider具体化后仍然是一个service,用于完成某种工作,只不过它是可配置的)。在config方法中,注入的函数参数对象名为系统生成的provider的名字helloProvider,自动添加了'provider'后缀。这里调用配置方法,这个配置会在未来使用该provider提供的功能时生效。最后,在controller定义中,注入了hello provider,并在sayHello方法中调用之,完成功能。这种模式应用十分广泛,一个例子是开源项目ui-router中的$state service以及对应的$stateProvider(provider的$get方法返回了一个$state对象,定义了go等方法和属性,使用过的开发者应该很熟悉了)。 只有provider类型的service可以注入到其他组件 中供其使用(如factory,constant等)。
值得注意的是,可以向controller注入service(可注入),但controller
不能被注入其他组件
中,因为
controller本身不是provider
。Angular维护着一个service名为$controller,定义并注册一个controller的工作实际上由该service的provider完成,当需要创建controller实例时,$controller会调用$injector的invoke方法完成依赖注入。(代码)
类似地,directive对应$compile service,filter对应$filter service,不再详述。
原文链接:https://www.f2er.com/angularjs/149766.html