这是我参与更文挑战的第 14 天,活动详情查看:更文挑战
关于依赖注入
什么是依赖注入? 他和控制反转又是什么关系?
看到知乎用户上的一个回答,我觉得放在这里很合适:
简单来说,a 依赖 b,但 a 不控制 b 的创建和销毁,仅使用 b,那么b的控制权交给 a 之外处理,这叫控制反转(IOC),
而 a 要依赖 b,必然要使用 b 的 instance,那么通过 a 的接口,把 b 传入;通过 a 的构造,把 b 传入;通过设置 a 的属性,把 b 传入;这个过程叫依赖注入(DI)。
实际上,控制反转是面向对象编程中一种设计原则,而依赖注入是其一种实现方式。其还有另外一种实现方式叫做依赖查找,由于和本文关系不大,这里就不再展开讲了。
di 其实包含了两件事情:依赖注入模式(又称控制反转)和实现依赖注入的框架。
为什么要使用 di?
依赖注入将物体的瞬时化与所封装的实际逻辑和行为区分开来。
这种模式有许多好处。例如:
明确的依赖性
所有依赖都作为构造参数传递,很容易理解特定对象如何依赖于环境的其余部分。
代码重用
此类对象在其他环境中更容易重复使用,因为它与特定的依赖关系的实现没有联系。
容易测试
因为测试本质上是关于在没有其他环境的情况下即时化一个对象。
在没有任何框架的情况下,遵循依赖注入范式是可能的。
但如果在这种情况下遵循依赖注入模式,你通常不得不写一个令人讨厌的 main()
方法,在那里你实例化所有对象并将它们连接在一起。
依赖注入框架使您免于这种样板。它可以保证应用程序之间的引用关联是声明式的,而不是命令式的。每个组件都声明其依赖项,并且框架确实会传递性地解决这些依赖项……
node-di
实现依赖注入的库很多,本文以 node-di 举例,展示实际的细节。
这是一个受到 angular.js 影响的,可用于 node.js 的 di 库。至今依然保持着每周上百万次下载量。
虽然作者声明已经停止维护,但还是可以从它的实现上了解关于依赖注入的细节。
首先看一下代码仓库
我们可以看到如下的四个文件:
标题 | 用途 |
---|---|
annotation.js | 注解 |
index.js | 文件入口 |
injector.js | 注入器 |
module.js | 模块 |
其中 index.js 的任务很简单,主要是将其余三个文件进行导出操作。
再看另外的几个文件。这里也可以结合库的示例来看
从示例看,首先引入了 di 框架,然后直接生成了一个注入器 Injector,而注入器的参数里面传入了一组初始化的配方。最后调用注入器的 invoke 方法执行。
好,确定了, Injector.js 就是整个库的核心内容了。这里我们先看一下其他的文件:
module.js
module.js 维护了一套 providers 数组,provider 支持 factory、value、type 三种类型,这三种类型分别代表返回方法、数值和对象。前面的三个方法将对应类型的值放在数组中。后面的 forEach 方法就是对 providers 数组遍历。
这里的 return this
是保证可以链式调用的关键。
annotation.js
而 annotation.js 中有两个方法 annotate 和 parse
annotate 先复制一套数组,然后将最后一个参数单独取出,整套数组设为最后一个参数的 $inject 属性,最后返回最后这个参数。
熟悉早期 angular.js 的同学可能会想到,这可能是符合下面的模式:
angular.controller(['$scope', '$timeout', function($scope, $timeout){}])
复制代码
具体有没有关系呢?这里先按下不表。
而 parse 方法就有点意思。实际上返回某个函数的参数列表。
比如说有一个求两数之和的函数,其中两个参数值为求和的参数。
function fn(a, b){
return a + b;
}
复制代码
首先判断传入的参数是否为函数,如果是函数,那么首先将这个函数转换为 string,然后使用正则表达式解析出参数列表
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
复制代码
这个正则表达式可以匹配下面两种格式的函数
// /* (((this is fine))) */ function(a, b) {}
// function abc(a) { /* (((this is fine))) */}
复制代码
最后得到的是:
然后切割字符串并依次去掉空格,最后返回一个参数 key 的列表。
injector.js
200 多行的文件,先看主要部分。初始化 Injector 时传入 module 数组,这个数组将会被循环,将 module 中每一条的值存放在最上面的 providers 中,保留在当前的对象里。
上文提到 module 中有三种类型 factory、type、value。这里按照 factoryMap 的方式执行对应转换方法。
Injector 初始化第二个参数为 parent,传入后会组成 Injector 依赖树。
invoke 调用方法,首先查找依赖 token 列表,然后循环调用 get 方法转换为真正的依赖。最后在调用 fn 方法时,将真正的依赖传入。
这里的 autoAnnotate
就是 annotation 中的 parse 函数。具体实现请翻到上面。
dependencies 中的 get 方法是获取真正依赖的核心。
首先从 providers 对象中获取实际的依赖。对于 token 包含分隔符的,递归去查找实际依赖。
如果获取不到,从当前 injector 中获取。
实例中获取不到,再从 parent injector 中去查找。
后面有循环依赖的判断。最终如果找不到,会执行如下方法,提示未找到 provider 的报错。
总结
实际上,依赖注入的解析过程,其实就是根据 provider token 获取依赖的过程。获取依赖时,先从 provider 显式指定的值查找,如果未找到,将会从当前实例向父级实例查找。这种情况下可能出现循环依赖。针对循环依赖需要特殊处理。
而指定 provider 列表时,特意分成了 factory、value、type 三种类型。这三种类型分别代表返回方法、数值和对象。内部会根据三种类型进行不同的处理。
希望能对各位有所帮助。