我喜歡那么一句話,就是編程就是管理復(fù)雜性葫督。也許你聽說過計(jì)算機(jī)世界就是一個(gè)巨大的抽象結(jié)構(gòu)竭鞍。我們只是簡(jiǎn)單地把東西包裝起來,一遍又一遍地生產(chǎn)新的工具橄镜。只需要試想一下偎快。你使用的語言具有內(nèi)置功能,它們可能是其他底層操作的抽象功能洽胶。JavaScript也一樣晒夹。
你遲早需要使用其他開發(fā)人員所寫的抽象。也就是說姊氓,你依賴于別人的代碼丐怯。我喜歡模塊沒有依賴,但這有點(diǎn)難實(shí)現(xiàn)翔横。即使你創(chuàng)建了一些像組件這樣的很不錯(cuò)的黑盒读跷,但你仍然有一部分是組合了所有的東西。這就是依賴注入的位置棕孙。如今舔亭,有效管理依賴關(guān)系的能力是絕對(duì)必要的。這篇文章總結(jié)了我對(duì)這個(gè)問題的看法蟀俊。
目標(biāo)
假設(shè)我們有兩個(gè)模塊钦铺。第一個(gè)是發(fā)出Ajax請(qǐng)求的服務(wù),第二個(gè)是路由器肢预。
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
我們還有一個(gè)函數(shù)需要這些模塊矛洞。
var doSomething = function(other) {
var s = service();
var r = router();
};
為了讓事情變得更有趣,這個(gè)函數(shù)需要再接受一個(gè)更多的參數(shù)烫映。當(dāng)然沼本,我們可以使用上面的代碼,但是這并不是很靈活锭沟。如果我們想使用ServiceXML
或ServiceJSON
怎么辦抽兆?或者,如果我們想要模擬一些模塊來進(jìn)行測(cè)試族淮,該怎么辦辫红?我們不能只編輯函數(shù)體凭涂。我們首先想到的是將依賴項(xiàng)作為參數(shù)傳遞給函數(shù)。例如:
var doSomething = function(service, router, other) {
var s = service();
var r = router();
};
通過這樣做贴妻,我們傳遞了我們想要的模塊的確切實(shí)現(xiàn)切油。然而,這帶來了一個(gè)新問題名惩。想象一下澎胡,如果我們的代碼中到處都是doSomething
。如果我們需要第三個(gè)依賴娩鹉,會(huì)發(fā)生什么攻谁。我們不能修改所有的函數(shù)調(diào)用。所以底循,我們需要一種工具來幫我們做到這一點(diǎn)巢株。這就是依賴注入器試圖解決的問題槐瑞。讓我們寫下幾個(gè)我們想要實(shí)現(xiàn)的目標(biāo):
- 我們應(yīng)該能夠去注冊(cè)依賴項(xiàng)
- 注入器應(yīng)該接受一個(gè)函數(shù)并返回一個(gè)以某種方式獲取所需的資源的函數(shù)熙涤。
- 我們不應(yīng)該寫太多,我們需要短小精悍(emmm)的語法困檩。
- 注入器應(yīng)該保持傳遞的函數(shù)的作用域祠挫。
- 傳遞的函數(shù)應(yīng)該能夠接受自定義參數(shù),而不僅僅是描述的依賴項(xiàng)悼沿。
一個(gè)不錯(cuò)的列表等舔,不是嗎?就讓我們一探究竟吧糟趾。
requirejs / AMD 方法
你可能已經(jīng)了解過requirejs慌植。它是解決依賴關(guān)系的一個(gè)很好的變體。
define(['service', 'router'], function(service, router) {
// ...
});
其思想是首先描述所需的依賴關(guān)系义郑,然后編寫函數(shù)蝶柿。參數(shù)的順序在這里當(dāng)然很重要。假設(shè)我們將編寫一個(gè)名為injector
的模塊非驮,它將接受相同的語法交汤。
var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
在繼續(xù)之前,我應(yīng)該澄清一下doSomething
函數(shù)的主體劫笙。我使用expect.js作為斷言庫(kù)芙扎,只是為了確保我正在編寫的代碼能夠按照我的預(yù)期工作。有點(diǎn)TDD方法填大。
我們的injector
模塊從這里開始戒洼。作為一個(gè)單例對(duì)象是很好的,因此它可以從應(yīng)用程序的不同部分完成它的工作允华。
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
}
}
非常簡(jiǎn)單的對(duì)象圈浇,它有兩個(gè)函數(shù)和一個(gè)變量作為存儲(chǔ)敷矫。我們要做的是檢查deps
數(shù)組并在dependencies
變量中搜索答案。其余的只是針對(duì)傳過去的func
參數(shù)調(diào)用.apply
方法汉额。
resolve: function(deps, func, scope) {
var args = [];
for(var i=0; i < deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
args.push(this.dependencies[d]);
} else {
throw new Error('Can\\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, args.concat(Array.prototype.slice.call(arguments, 0)));
}
}
如果有任何的作用域它都有效地使用曹仗。Array.prototype.slice.call(arguments, 0)
是將arguments
變量轉(zhuǎn)換為實(shí)際數(shù)組所必需的。這個(gè)實(shí)現(xiàn)的問題是蠕搜,我們必須編寫兩次所需的組件怎茫,并且我們不能真正混合它們的順序。其他自定義參數(shù)始終位于依賴項(xiàng)之后妓灌。
reflection(反射)方法
根據(jù)Wikipedia轨蛤,反射是程序在運(yùn)行時(shí)檢查和修改對(duì)象的結(jié)構(gòu)和行為的能力。簡(jiǎn)單的說就是虫埂,在JavaScript上下文中祥山,就是讀取對(duì)象或函數(shù)的源代碼并分析它。讓我們從一開始就得到doSomething
函數(shù)掉伏。如果你對(duì)doSomething.toString()
進(jìn)行輸出缝呕,你將會(huì)得到下面字符串:
"function (service, router, other) {
var s = service();
var r = router();
}"
將該方法作為字符串使我們能夠獲取預(yù)期的參數(shù)。 而且斧散,更重要的是供常,他們的名字。這就是Angular用來實(shí)現(xiàn)依賴注入的方法鸡捐。我做了一點(diǎn)小小的改動(dòng)栈暇,得到了一個(gè)正則表達(dá)式,它直接從Angular的代碼中導(dǎo)出參數(shù)箍镜。
/^function\\s*[^\\(]*\\(\\s*([^\\)]*)\\)/m
我們可以將resolve
類更改為以下內(nèi)容:
resolve: function() {
var func, deps, scope, args = [], self = this;
func = arguments[0];
deps = func.toString().match(/^function\\s*[^\\(]*\\(\\s*([^\\)]*)\\)/m)[1].replace(/ /g, '').split(',');
scope = arguments[1] || {};
return function() {
var a = Array.prototype.slice.call(arguments, 0);
for(var i=0; i < deps.length; i++) {
var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {}, args);
}
}
我們根據(jù)函數(shù)的定義運(yùn)行RegExp源祈。其結(jié)果是:
["function (service, router, other)", "service, router, other"]
所以,我們只需要第二項(xiàng)色迂。一旦我們清理了空的空格并拆分了字符串香缺,我們就填充了deps
數(shù)組。還有一個(gè)變化:
var a = Array.prototype.slice.call(arguments, 0);
...
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
我們循環(huán)遍歷依賴項(xiàng)脚草,如果缺少什么赫悄,我們將嘗試從arguments
對(duì)象中獲取它。值得慶幸的是馏慨,如果數(shù)組為空埂淮,則shift
方法返回undefined
。他不會(huì)拋出一個(gè)錯(cuò)誤写隶。新版本的injector
可以這樣使用:
var doSomething = injector.resolve(function(service, other, router) {
expect(service().name).to.be('Service');
expect(router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
不需要重復(fù)寫依賴項(xiàng)倔撞,我們可以混合它們的順序。它仍然有效慕趴,我們復(fù)制了Angular的魔法痪蝇。
然而鄙陡,世界并不完美,反射式注入有一個(gè)很大的問題躏啰。壓縮將打破我們的邏輯趁矾。那是因?yàn)樗淖兞藚?shù)的名稱,我們將無法解析依賴關(guān)系给僵。例如:
var doSomething=function(e,t,n){var r=e();var i=t()}
這就是我們的doSomething
函數(shù)被傳遞給了一個(gè)壓縮器毫捣。 Angular團(tuán)隊(duì)提出的解決方案看起來像這樣:
var doSomething = injector.resolve(['service', 'router', function(service, router) {
}]);
它看起來像我們開始時(shí)的東西。我個(gè)人無法找到更好的解決方案帝际,于是決定將這兩種方法混合使用蔓同。這就是注入器(injector)的最終版本。
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function() {
var func, deps, scope, args = [], self = this;
if(typeof arguments[0] === 'string') {
func = arguments[1];
deps = arguments[0].replace(/ /g, '').split(',');
scope = arguments[2] || {};
} else {
func = arguments[0];
deps = func.toString().match(/^function\\s*[^\\(]*\\(\\s*([^\\)]*)\\)/m)[1].replace(/ /g, '').split(',');
scope = arguments[1] || {};
}
return function() {
var a = Array.prototype.slice.call(arguments, 0);
for(var i=0; i < deps.length; i++) {
var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {}, args);
}
}
}
resolve
方法接受兩個(gè)或三個(gè)參數(shù)蹲诀。如果是兩個(gè)斑粱,就像我們最近寫的一樣。是脯爪,如果有三個(gè)參數(shù)则北,它將獲得第一個(gè)參數(shù),然后解析它并填充deps
數(shù)組披粟。下面是測(cè)試用例:
var doSomething = injector.resolve('router,,service', function(a, b, c) {
expect(a().name).to.be('Router');
expect(b).to.be('Other');
expect(c().name).to.be('Service');
});
doSomething("Other");
你可能會(huì)注意到有兩個(gè)逗號(hào)一個(gè)接一個(gè)咒锻。這不是手誤∈靥耄空值實(shí)際上表示"Other"
參數(shù)。這就是我們?nèi)绾慰刂茀?shù)順序的蒿辙。
直接注入到作用域
有時(shí)我會(huì)使用第三種注入方式拇泛。它涉及到對(duì)函數(shù)作用域(或者換句話說,就是this
對(duì)象)的操作思灌。所以俺叭,它并不總是合適的。
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
scope = scope || {};
for(var i=0; i < deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
scope[d] = this.dependencies[d];
} else {
throw new Error('Can\\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
}
}
}
我們所做的就是將依賴項(xiàng)附加到作用域上泰偿。這里的好處是開發(fā)人員不應(yīng)該將依賴項(xiàng)作為參數(shù)編寫熄守。他們只是函數(shù)作用域的一部分。
var doSomething = injector.resolve(['service', 'router'], function(other) {
expect(this.service().name).to.be('Service');
expect(this.router().name).to.be('Router');
expect(other).to.be('Other');
});
doSomething("Other");
最后的話
依賴注入是我們都在做的事情之一耗跛,但從來沒有想過裕照。即使你沒有聽說過這個(gè)詞,你可能已經(jīng)用過幾百萬次了调塌。
本文中提到的所有示例都可以在這里看到晋南。
本文原文:JavaScript中的依賴注入