AngularJS中的依赖注入

本文深入探讨AngularJS的依赖注入机制,包括setter注入与constructor注入的区别,AngularJS中依赖注入的实现方式,以及如何在组件中使用依赖注入。同时,文章详细介绍了AngularJS的injector子系统,以及在模块加载过程中config和run方法的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、关于依赖注入

依赖注入式AngularJS的重要特性之一,有关概念和定义参考维基百科。依赖注入简化了Angular解析模块/组件之间依赖的过程。通常一个组件要获得它的依赖,有三种方式:

直接创建出依赖,如使用new操作符
能够查找到依赖,如引用全局变量
在需要的地方传入依赖
第三种的优势在于组件省去了定义/定位依赖的过程,也使得依赖的耦合度降低,可扩展性更强。依赖注入主要有两种形式(看这里):setter注入和constructor注入。前者的代表是Spring框架的setter方式,AngularJS则使用的是constructor注入。这里简单说一下二者的区别,setter注入顾名思义,首先使用一个无参的默认构造器构造对象,然后使用setter方法将依赖注入到新对象中。这种方式的一个缺陷是在编译时并不知道对象之间的依赖关系,依赖解析由某种框架在运行时完成。如果在配置依赖时有遗漏,则在运行时会报空引用错误。对于constructor注入,在构造对象时,将依赖的组件以参数形式传入构造器,传入组件所依赖的组件以相同的方式构造,依此类推,确保依赖链条最顶端的组件首先构造,一直到当前需要注入的对象构造完毕。该方式避免了依赖的遗漏。

二、AngularJS中的依赖注入
Angular的injector子系统负责创建组件,解析依赖,并将其按需提供给其他组件。每个Angular应用都有一个injector。Angular在应用的启动阶段(bootstrap,参见前面的文章)会创建一个injector:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
var injector = angular.injector([‘ng’, ‘myApp’]);
injector方法的数组参数定义了可以提供依赖组件的模块,一般会提供模块’ng’和应用首先加载的模块’myApp’。查看源码发现,injector方法实际上实现为createInjector方法,由bootstrap方法调用。createInjector内部调用createInternalInjector方法返回真正的injector对象,返回结果如下:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
return {
invoke: invoke,
instantiate: instantiate,
get: getService,
annotate: createInjector.$$annotate,
has: function(name) {
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
}
};

其中前三个方法很重要,例如可以通过get方法获得一个需要注入的组件:

[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
var comp = injector.get(‘component’);
在Angular中,依赖注入可谓无孔不入。通常在两种场景(函数)下会使用到依赖注入:

工厂方法定义的组件(components):如directive,factory,filter,provider,controller等。这些工厂函数需要注册到某个模块上。controller比较特殊,它虽然也是一种组件,但是特别之处是它与某个DOM元素关联,因此可以注入 s c o p e s e r v i c e , 而 其 他 组 件 只 能 注 入 scope service,而其他组件只能注入 scopeservicerootScope service。
模块提供的run/config方法。
我们称定义组件的工厂方法和run/config方法是可注入的。
Angular支持三种定义依赖注入的方式:
数组标注:最常用且推荐的方式。例如:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
myApp.controller(‘smallCatCtrl’, [' s c o p e ′ , f u n c t i o n ( scope', function( scope,function(scope){
$scope.sayCat = function(){
alert(‘I Love Circle!’);
}
}]);

i n j e c t 属 性 标 注 : 这 种 方 式 通 过 工 厂 方 法 的 inject属性标注:这种方式通过工厂方法的 injectinject属性声明依赖的组件,主要用于js被压缩/混淆时,变量被重命名的情况。例如:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
var MyController = function( s c o p e , m y S e r v i c e ) / / . . . M y C o n t r o l l e r . scope, myService) { // ... } MyController. scope,myService)//...MyController.inject = [’$scope’, ‘myService’];
myApp.controller(‘MyController’, MyController);

隐式标注:这种方法最简易,Angular会通过工厂方法的参数名,推断找到依赖的组件,但不能用于js被压缩/混淆的情况,因为这种情况下方法的参数可能被重命名导致Angular无法定位依赖。例如:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
myApp.controller(‘smallCatCtrl’, function($scope){
KaTeX parse error: Expected 'EOF', got '}' at position 73: …!'); } }̲); 另外,Angular…injector 即可,有意思的是它知道如何注入自己。通过使用$injector service的invoke方法,可以将组件注入到任何方法中,如:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
var myFunction = function(hello) {
hello(‘My Cat’);
};
$injector.invoke(myFunction);
这段代码将hello service注入到myFunction函数作为其一个参数。我们定义组件(如controller,directive,filter,factory)所使用的工厂方法就是通过invoke函数注入依赖的。例如在Angular启动时,bootstrap方法会有如下逻辑:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
injector.invoke([‘ r o o t S c o p e ′ , ′ rootScope', ' rootScope,rootElement’, ‘ c o m p i l e ′ , ′ compile', ' compile,injector’,
function bootstrapApply(scope, element, compile, injector) {
scope.KaTeX parse error: Expected '}', got 'EOF' at end of input: … 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:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
myApp.config(function($provide) {
KaTeX parse error: Expected '}', got 'EOF' at end of input: …) { this.get = function() {
return function(name) {
alert("Hello, cat " + name);
};
};
});
});
这里的config方法稍后再谈。 p r o v i d e 是 A n g u l a r 内 部 提 供 的 一 个 s e r v i c e , 用 来 定 义 p r o v i d e r 。 provide 是Angular内部提供的一个service,用来定义provider。 provideAngularserviceproviderget方法返回了真正的service。为了简单,使用具体的provider类型进行定义也可以,这样还可省去get方法:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
myApp.config(function($provide) {
$provide.factory(‘sayCat’, function() {
return function(name) {
alert(“Hello, Cat” + name);
};
});
});
这样就定义了一个factory service。如果把这套定义逻辑抽象出来,就成了Angular模块的一系列定义service的方法:
[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
myMod.factory(“sayHello”, …);
myMod.service(“sayHello”, …);
myMod.value(“Cat”, …);
这些方法在调用时实际执行仍然是前面代码段所示的完整版本(有get和provider方法)。
下面说说config/run方法。Angular在加载模块时经过两个阶段:config和run。传入config函数的方法会在当前模块加载时执行;传入run函数的方法会在所有模块加载结束后执行。因此,在config阶段即可配置多个provider,但是在config阶段,只有provider可以注入,因此自定义的service无法注入到config中。这也好理解,因为config阶段是对service进行配置的而不是使用service本身。在前面的代码中, p r o v i d e s e r v i c e 本 身 就 是 一 个 p r o v i d e r ( 对 于 A n g u l a r 应 用 来 讲 相 当 于 一 个 ′ 元 p r o v i d e r ′ ) , 在 模 块 加 载 时 会 调 用 provide service本身就是一个provider(对于Angular应用来讲相当于一个'元provider'),在模块加载时会调用 provideserviceproviderAngularproviderprovide的provider方法定义一个新的provider。再看一个具体的例子:

[javascript] view plain copy

在CODE上查看代码片派生到我的代码片
define([‘angular’], function(angular){
return angular.module(‘myCat’, [])
.provider(‘hello’, function() {
var firstName = ‘’;
this.makeName = function(first){
firstName = first;
}
this.KaTeX parse error: Double superscript at position 109: …firstName + ' '̲ + name); …scope’,
' q ′ , ′ h e l l o ′ , f u n c t i o n ( q', 'hello', function( q,hello,function(scope, $defer, hello){
$scope.catName = “”;
$scope.sayHello = function(){
hello(‘Jiang’);//output ‘Hello, Circle Jiang’
};
}]
);
});

在myCat模块中定义了一个名为hello的provider。该provider提供了一个makeName配置方法,用于设置firstName。 g e t 方 法 返 回 一 个 函 数 用 于 打 印 一 句 话 , 这 是 该 p r o v i d e r 的 核 心 功 能 ( 记 住 p r o v i d e r 具 体 化 后 仍 然 是 一 个 s e r v i c e , 用 于 完 成 某 种 工 作 , 只 不 过 它 是 可 配 置 的 ) 。 在 c o n f i g 方 法 中 , 注 入 的 函 数 参 数 对 象 名 为 系 统 生 成 的 p r o v i d e r 的 名 字 h e l l o P r o v i d e r , 自 动 添 加 了 ′ p r o v i d e r ′ 后 缀 。 这 里 调 用 配 置 方 法 , 这 个 配 置 会 在 未 来 使 用 该 p r o v i d e r 提 供 的 功 能 时 生 效 。 最 后 , 在 c o n t r o l l e r 定 义 中 , 注 入 了 h e l l o p r o v i d e r , 并 在 s a y H e l l o 方 法 中 调 用 之 , 完 成 功 能 。 这 种 模 式 应 用 十 分 广 泛 , 一 个 例 子 是 开 源 项 目 u i − r o u t e r 中 的 get方法返回一个函数用于打印一句话,这是该provider的核心功能(记住provider具体化后仍然是一个service,用于完成某种工作,只不过它是可配置的)。在config方法中,注入的函数参数对象名为系统生成的provider的名字helloProvider,自动添加了'provider'后缀。这里调用配置方法,这个配置会在未来使用该provider提供的功能时生效。最后,在controller定义中,注入了hello provider,并在sayHello方法中调用之,完成功能。这种模式应用十分广泛,一个例子是开源项目ui-router中的 getproviderproviderserviceconfigproviderhelloProviderprovider使providercontrollerhelloprovidersayHello广uirouterstate service以及对应的 s t a t e P r o v i d e r ( p r o v i d e r 的 stateProvider(provider的 stateProviderproviderget方法返回了一个$state对象,定义了go等方法和属性,使用过的开发者应该很熟悉了)。只有provider类型的service可以注入到其他组件中供其使用(如factory,constant等)。

值得注意的是,可以向controller注入service(可注入),但controller不能被注入其他组件中,因为controller本身不是provider。Angular维护着一个service名为 c o n t r o l l e r , 定 义 并 注 册 一 个 c o n t r o l l e r 的 工 作 实 际 上 由 该 s e r v i c e 的 p r o v i d e r 完 成 , 当 需 要 创 建 c o n t r o l l e r 实 例 时 , controller,定义并注册一个controller的工作实际上由该service的provider完成,当需要创建controller实例时, controllercontrollerserviceprovidercontrollercontroller会调用 i n j e c t o r 的 i n v o k e 方 法 完 成 依 赖 注 入 , injector的invoke方法完成依赖注入, injectorinvokecontrollerProvider的$get方法返回一个实例。

类似地,directive对应 c o m p i l e s e r v i c e , f i l t e r 对 应 compile service,filter对应 compileservicefilterfilter service,不再详述。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值