热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

[AngularJS面面观]19.依赖注入---Provider是个啥

在前面介绍angular中依赖注入相关的概念和细节时,非常多次提到了provider这个概念,每次提到都会让大家再等等,再等等。现在再也等不了啦,从本篇文章开始就会陆续介绍provider和一些

在前面介绍angular中依赖注入相关的概念和细节时,非常多次提到了provider这个概念,每次提到都会让大家再等等,再等等。现在再也等不了啦,从本篇文章开始就会陆续介绍provider和一些基于provider的高层方法,比如servicefactory等等。

provider是什么?

通过对象声明provider

首先,我们来看看provider是什么。在angular中,provider就是知道如何处理依赖关系的一类对象。为啥说是一类对象呢,因为只要一个对象实现了provider规定的契约,那么该对象就可以被称作provider。这个契约也非常的简单:提供一个带有返回值的$get方法即可。

比如下面的对象就是符合要求的provider

{
$get: function() {
return 'aConstant';
}
}

上面的对象定义了一个$get方法,该方法返回了一个字符串作为返回值。

现在,我们定义了一个最简单的provider,该如何把这个provider注册到模块中呢?没错,还是通过module实例提供的provider方法:

var app = angular.module('test', []);

// 通过provider提供一个常量的获取方法
app.provider('a', {
$get: function() {
return 'aConstant';
}
});

// 直接通过constant定义一个常量
app.constant('b', 'bConstant');

在上述代码中,我们分别使用providerconstant方法定义了常量。比较一下两种方法,看看他们有什么区别?可能会有同学说不就是定义一个常量,有必要大费周章吗?确实,使用provider来定义常量从步骤上而言会繁琐一些,但是它也增加了代码的灵活性不是吗?在计算机领域,有一句名言:”计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”。这句话放在这里也同样试用,如果因为种种原因常量的定义不能在直接在代码中确定,那么将这个定义的过程延迟到$get方法中不就是一种合理的解决方案吗?这里provider的作用就是提供这一间接的中间层。

现在我们回头看看module类型中是如何定义provider方法的:

provider: invokeLaterAndSetModuleName('$provide', 'provider')

// invokeLaterAndSetModuleName本身也是一个函数,返回另一个函数
function invokeLaterAndSetModuleName(provider, method) {
return function(recipeName, factoryFunction) {
if (factoryFunction && isFunction(factoryFunction)) factoryFunction.$$moduleName = name;
invokeQueue.push([provider, method, arguments]);
return moduleInstance;
};
}
});

当然,为了使provider能够起到作用,它的$get方法也是能够声明其依赖的,比如这样:

var app = angular.module('test', []);

// 通过provider提供一个常量的获取方法
app.provider('a', {
// 声明依赖关系,最终得到的a常量为"aConstantbConstant"
$get: function(b) {
return 'aConstant' + b;
}
});

// 直接通过constant定义一个常量
app.constant('b', 'bConstant');

函数的柯里化

看上去很奇怪,一个函数返回了另外一个函数。其实这里涉及到一个叫做”函数的柯里化“的概念,是一种常见的将接受多个参数的函数转化为只接受一个或者有限几个的函数的方法。为了分析这个问题,我们不妨假设一下现在并没有invokeLaterAndSetModuleName这个函数。那么要完成既定任务,provider方法对应的函数签名就必须变成这样了:

function(provider, method, recipeName, factoryFunction) {}

四个参数有点多了,而且复用性不佳。使用了柯里化将四个参数的函数转换为两个参数的函数,同时还能够在函数内部完成一些额外的任务,比如在invokeLaterAndSetModuleName函数中就还完成了一项设置module名字的任务。最后,函数返回的是moduleInstance,意味着它能够支持所谓的”链式编程”。就是可以连续地在module上定义多个服务,比如这样:

angular.module('test', []).constant('a', 'aConstant').constant('b', 'bConstant');

定义了provider后,在什么时候会运行呢?在module被加载的时候,会执行下面这段代码(关于module加载和任务队列,可以参考这篇文章初识注入器):

function runInvokeQueue(queue) {
var i, ii;
for (i = 0, ii = queue.length; i var invokeArgs = queue[i],
// 得到$provide对象
provider = providerInjector.get(invokeArgs[0]);

// invokeArgs的对应关系:[0] - '$provide', [1] - 'provider', [2] - [providerName, providerDef]
provider[invokeArgs[1]].apply(provider, invokeArgs[2]);
}
}

执行任务队列的关键就在于上述for循环中的最后一行:provider[invokeArgs[1]].apply(provider, invokeArgs[2]);,它实际上的调用的是$provide对象上的provider方法:

function provider(name, provider_) {
// 确保name不等于hasOwnProperty
assertNotHasOwnProperty(name, 'service');
// 如果提供的是provider_是函数或者数组,直接使用injector实例化
if (isFunction(provider_) || isArray(provider_)) {
provider_ = providerInjector.instantiate(provider_);
}
// 如果provider_没有提供$get方法,抛出异常
if (!provider_.$get) {
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
}
// 将provider_放入到providerCache中
return providerCache[name + providerSuffix] = provider_;
}

可以发现,最终provider会调用providerInjector.instantiate方法进行实例化。而实例化的过程我们在之前的文章中已经介绍过了,它能够通过注解信息找到需要的被托管对象。需要注意的是,instantiate方法的调用只有当provider_参数是函数或者数组类型时才会发生。像我们之前那样通过object的方式定义provider并不会触发instantiate的调用。这样只会将该object放入到providerCache中供未来使用。

通过构造器函数声明provider

那么,从代码中可以看出provider的定义方式并非只有通过object一种方法,还能够通过构造器函数或者数组的形式定义:

var app = angular.module('test', []);

// 通过provider提供一个常量的获取方法,使用构造器函数
app.provider('a', function AProvider() {
this.$get = function() {
return 'aConstant';
};
});

函数的名字并非一定要是AProvider,任何别的什么名字都可以。但是这里作为一种约定,使用它能够让代码的可读性更好。可以看到,只要这个AProvider类型中提供了一个名为$get的方法即可。这一点和使用object来定义provider是一样一样的。

所以通过构造器函数的方式来定义provider的话,就会执行上述代码中的providerInjector.instantiate(provider_)了,而我们已经知道instantiate方法是有能力来处理依赖问题的。因此在provider的构造器函数中,可以定义需要依赖的函数,比如这样:

var app = angular.module('test', []);

app.constant('b', 'bConstant');

// 通过provider提供一个常量的获取方法,使用构造器函数并声明依赖
app.provider('a', function AProvider(b) {
this.$get = function() {
// 那么最终a常量的定义就是: aConstantbConstant
return 'aConstant' + b;
};
});

两种provider声明方式的区别

现在,我们目前有两种方式来定义provider
1. 通过对象的方式,该对象需要有一个类型为函数的$get属性。
2. 通过构造器函数的方式,该函数对象内部有一个$get方法。

那么这两种方式是否是一样的,在任何场合下都能够互换呢?答案是否定的,其实答案在上述介绍provider函数的源码时就已经揭晓了,让我们再来看看该函数的定义:

function provider(name, provider_) {
// 确保name不等于hasOwnProperty
assertNotHasOwnProperty(name, 'service');
// 如果提供的是provider_是函数或者数组,直接使用injector实例化
if (isFunction(provider_) || isArray(provider_)) {
provider_ = providerInjector.instantiate(provider_);
}
// 如果provider_没有提供$get方法,抛出异常
if (!provider_.$get) {
throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name);
}
// 将provider_放入到providerCache中
return providerCache[name + providerSuffix] = provider_;
}

区别就在于懒加载是否能够实现。

第一种使用对象的provider声明方式并不会导致instantiate方法的执行。而第二种使用构造器函数的方式则会触发instantiate方法。因此,第一种方式能够实现被托管对象的懒加载,而第二种方式则不能,如果声明为构造器函数则总是会急不可耐地调用instantiate方法完成它的所有依赖和它本身的实例化。所谓懒加载,就是当真正地需要某个被注入器托管的对象时才会去创建。而这个真正去创建的过程就是invoke/instantiate方法去做的事情。关于懒加载的相关讨论,可以参考这一篇文章依赖注入 — 注入器中如何管理对象。

那么是谁来实例化这个我们定义的构造器函数形式的provider呢?在源码中是一个叫做providerInjector的注入器,它和我们通常所说的注入器是一个东西吗?从源代码来看:

function createInjector(modulesToLoad, strictDi) {
strictDi = (strictDi === true);
var INSTANTIATING = {},
providerSuffix = 'Provider',
path = [],
loadedModules = new HashMap([], true),

// provider cache以及provider injector
providerCache = {
// ......
},
providerInjector = (providerCache.$injector =
createInternalInjector(providerCache, function(serviceName, caller) {
if (angular.isString(caller)) {
path.push(caller);
}
throw $injectorMinErr('unpr', "Unknown provider: {0}", path.join(' <- '));
})),

// instance cache以及instance injector
instanceCache = {},
protoInstanceInjector =
createInternalInjector(instanceCache, function(serviceName, caller) {
var provider = providerInjector.get(serviceName + providerSuffix, caller);
return instanceInjector.invoke(
provider.$get, provider, undefined, serviceName);
}),
instanceInjector = protoInstanceInjector;

// ......

// 返回的是instance injector
return instanceInjector;

// 其它内部函数...
}

答案很明确,并不是一个东西。返回的是instance注入器,而provider注入器只是存在于内部的另外一个注入器。

真是一个问题接着一个问题,解决了一个问题又引出了另外一个问题。不过这也正是程序开发的有意思之处。世界这么复杂,怎么可能三言两语就讲的明白呢。

那么我们的问题就转变成了为何要如此设计呢,instance注入器和provider注入器有什么区别和联系,它们之间存在交互行为吗?这些问题留待下一篇文章解答。


推荐阅读
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 本文介绍了在Android开发中使用软引用和弱引用的应用。如果一个对象只具有软引用,那么只有在内存不够的情况下才会被回收,可以用来实现内存敏感的高速缓存;而如果一个对象只具有弱引用,不管内存是否足够,都会被垃圾回收器回收。软引用和弱引用还可以与引用队列联合使用,当被引用的对象被回收时,会将引用加入到关联的引用队列中。软引用和弱引用的根本区别在于生命周期的长短,弱引用的对象可能随时被回收,而软引用的对象只有在内存不够时才会被回收。 ... [详细]
  • Ihaveaworkfolderdirectory.我有一个工作文件夹目录。holderDir.glob(*)>holder[ProjectOne, ... [详细]
  • 本文介绍了Redis的基础数据结构string的应用场景,并以面试的形式进行问答讲解,帮助读者更好地理解和应用Redis。同时,描述了一位面试者的心理状态和面试官的行为。 ... [详细]
  • 1,关于死锁的理解死锁,我们可以简单的理解为是两个线程同时使用同一资源,两个线程又得不到相应的资源而造成永无相互等待的情况。 2,模拟死锁背景介绍:我们创建一个朋友 ... [详细]
  • 深入理解Kafka服务端请求队列中请求的处理
    本文深入分析了Kafka服务端请求队列中请求的处理过程,详细介绍了请求的封装和放入请求队列的过程,以及处理请求的线程池的创建和容量设置。通过场景分析、图示说明和源码分析,帮助读者更好地理解Kafka服务端的工作原理。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 重入锁(ReentrantLock)学习及实现原理
    本文介绍了重入锁(ReentrantLock)的学习及实现原理。在学习synchronized的基础上,重入锁提供了更多的灵活性和功能。文章详细介绍了重入锁的特性、使用方法和实现原理,并提供了类图和测试代码供读者参考。重入锁支持重入和公平与非公平两种实现方式,通过对比和分析,读者可以更好地理解和应用重入锁。 ... [详细]
  • STL迭代器的种类及其功能介绍
    本文介绍了标准模板库(STL)定义的五种迭代器的种类和功能。通过图表展示了这几种迭代器之间的关系,并详细描述了各个迭代器的功能和使用方法。其中,输入迭代器用于从容器中读取元素,输出迭代器用于向容器中写入元素,正向迭代器是输入迭代器和输出迭代器的组合。本文的目的是帮助读者更好地理解STL迭代器的使用方法和特点。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • Android JSON基础,音视频开发进阶指南目录
    Array里面的对象数据是有序的,json字符串最外层是方括号的,方括号:[]解析jsonArray代码try{json字符串最外层是 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • python中安装并使用redis相关的知识
    本文介绍了在python中安装并使用redis的相关知识,包括redis的数据缓存系统和支持的数据类型,以及在pycharm中安装redis模块和常用的字符串操作。 ... [详细]
  • 广度优先遍历(BFS)算法的概述、代码实现和应用
    本文介绍了广度优先遍历(BFS)算法的概述、邻接矩阵和邻接表的代码实现,并讨论了BFS在求解最短路径或最短步数问题上的应用。以LeetCode中的934.最短的桥为例,详细阐述了BFS的具体思路和代码实现。最后,推荐了一些相关的BFS算法题目供大家练习。 ... [详细]
author-avatar
都百美丽人生
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有