AngularJS特性研究之依赖注入

说到AngularJS,大家都知道它有几个非常著名的特性,分别是:数据双向绑定、依赖注入(DI)、指令系统。这次我想来了解一下依赖注入背后的原理是怎样的。

什么是依赖注入

依赖这个名词很好理解,就是我需要一个东西,这个东西并不是我自己的,而是来自其它地方的;注入则表示,现在我需要它,别人把这个东西呈给我。

代码举例:

1
2
3
4
5
6
7
8
9
// a.js
function a() {}
a.prototype.method = function() { console.log('todo something'); }
// b.js
require('a.js)
a.method()

当b文件需要使用a文件里的方法时,只需要通过require的方式将其引入进来。正确开发的时候不能这么写,这里只是起来解释什么是依赖注入。

依赖注入的好处

依赖注入是一种设计模式,它移除了硬编码依赖,因此使得我们可以在运行中随时移除并改变依赖项目。

在运行过程中能够修改依赖项目的能力允许我们创建隔离环境,这对于测试来说是非常理想的。我们可以用测试环境中的一个冒牌对象来替换生产环境中的一个真实对象。

从功能上来说,这种模式通过自动提前查找依赖以及为依赖提供目标,以此将依赖资源注入到需要它们的地方。

Angular中的依赖注入

下面让我们来看看AngularJS中如何使用依赖注入的:

1
2
3
var FirstController = ($scope) => {
$scope.a = 1
}

上面的代码只要是有点Angular基础的都很容易看懂,FirstController控制器需要使用$scope,看起来貌似挺正常。

再来一个示例:

1
2
3
4
5
6
7
8
var FirstController = ($scope, $http) => {
$scope.a = 1
$http.get('url')
}
FirstController()

代码比原来增加了一个$http服务,这个时候问题来了,如果此时我们调用FirstController会发生什么呢?不用想,肯定会报错。但是在AngularJS的世界里,它会知道此时需要传入$scope和$http,并且正确的将其传入。这就是Angular的依赖注入机制,我们可以试着来模拟一下,Let’s do it。

模拟Angular依赖注入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var FirstController = ($scope,$http) => {
$scope.a = 1;
$http.get('');
};
// 依赖注入器
class Injector {
constructor() {
this.dependencies = {};
}
register(key, value) {
this.dependencies[key] = value;
}
resolve(deps, controller) {
let arr = [];
deps.forEach(dep => {
// 判断该模块是否被注册
if (this.dependencies.hasOwnProperty(dep)) {
arr.push(this.dependencies[dep]);
}
})
return () => {
controller.apply({}, arr);
}
}
}

在这里创建了里面injector,dependencies对象用来保存通过injector注册的模块,register方法实现注册模块,resolve方法用于为控制器注入所需的依赖。

1
2
3
4
var injector = new Injector();
injector.register('$http', {'get': value => console.log(value);});
injector.register('$scope', {'a':''});

我们先注册几个模块,Angular在其内部也实现了这个过程。

现在可以开始注入了。

1
2
3
4
5
6
7
8
9
10
var FirstController = ($scope,$http) => {
$scope.a = 1;
$http.get('load data successful');
};
FirstController = injector.resolve(['$scope', '$http'], FirstController);
//Run Controller
FirstController();

运行正常,我们得到了意料的结果。
DI

不过,这并没有结束,现在还有两个问题需要我们解决

  • 如果resolve中传入的deps顺序有误怎么办?
  • 写书写AnuglarJS代码时候,不需要我们去resolve

通过翻看Angular的源代码,发现了它的解决办法:将函数代码字符串化,然后通过正则匹配出需要的依赖。

但是具体是怎么实现呢?我们继续改造injector!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
resolve(controller) {
let FN_ARGS = /^\s*[^\(]*\(\s*([^\)]*)\)/m;
let STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
let fnText = controller.toString().replace(STRIP_COMMENTS, '');
let argDecl = fnText.match(FN_ARGS);
let deps = argDecl[1].split(',');
let arr = [];
deps.forEach(dep => {
// 判断该模块是否被注册
if (this.dependencies.hasOwnProperty(dep)) {
arr.push(this.dependencies[dep]);
}
})
return () => {
controller.apply({}, arr);
}
}

改造后的resolve不再需要我们传依赖数据,而是自己得出依赖并且自动注入

1
FirstController = injector.resolve(FirstController);

不再传入数组,仅仅传入一个控制器,可以看到结果依然是正确的。

DI

总结

写到这里,实现了一个微型的Injector,然而Angular背后的实现原比这个要复杂的多。如果文章中有什么写的不对的地方,欢迎指正。