一、准備
angular的源碼一份,我這里使用的是v1.4.7。源碼的獲取,請參考我另一篇博文:angular源碼分析:angular源代碼的獲取與編譯環境安裝
二、什么是依賴注入
據我所知,依賴注入的概念最早使用時在java編程中。依賴注入和控制反轉差不多是一個概念,是編程中一種重要的解耦手段。依賴注入不是目的,它是一系列工具和手段,最終的目的是幫助我們開發出松散耦合、可維護、可測試的代碼和程序。這條原則的做法是大家熟知的面向接口,或者說是面向抽象編程。具體含義是:當某個角色(可能是一個對象實例,調用者)需要另一個角色(另一個對象實例,被調用者)的協助時,在傳統的程序設計過程中,通常由調用者來創建被調用者的實例。但在angular里,創建被調用者的工作不再由調用者來完成,因此稱為控制反轉;創建被調用者實例的工作通常由angular來完成,然后注入調用者,因此也稱為依賴注入。
寫了這么多,舉個具體的例子:
angular.module('myService',['ng']);
angular
.module('myService')
.factory('currentTime',function($window){
return function(){
var now = $window.Date();
return now;
};
});
在上面這段代碼中,我首先創建了一個"myService"的模塊,讓后在這個模塊上創建了一個叫"currentTime"的服務。
我編寫的這個服務是依賴於另一個服務"$window"。如果我的服務"currentTime",在其他地方被另外的代碼依賴,那么angular的框架就會自動去查找是否存在$window這樣的服務,如果存在,就會將其傳入到我代碼中定義的工廠方法中,來實例化一個currentTime,並且將currentTime記錄下來,下次需要就不再實例化,而是直接給與;如果不存在,就會先去實例化$window這個服務。
這里需要注意的在angular中,服務都是單實例形式存在(可以利用這點來完成組件間的通信),控制器(controller)是可以多實例的。
這么做好:
1.松散耦合。編寫組件的標准變成:有且只有依賴於被 “依賴注入”的對象,不允許依賴於其他。這樣,一個對象與外界的“扇入”就被限制在了所依賴的對象范圍內,是一種可控的狀態,“扇出”同樣只能在被依賴的時候才會建立。
2.可維護。由於上面的性質,決定了依賴於某對象的對象可以不用管依賴住對象的內部實現。依賴對象對象外的只有接口和實現的功能,當需求發生變化的時候,我就需要修改對象的依賴模塊就行了。
3.可測試。想想在對某個模塊做單元測試,在有依賴注入的情況下,只需要構建理想的被依賴模塊,注入到這個要測試的模塊中,然后檢查輸出結構就行了。
三、js中依賴注入的原理
1.js中的函數有一個默認的方法toString()
可以將函數的作為字符串輸出。舉例:
function test(params){
//在控制台輸出
console.log('this is a test');
}
var out = test.toString();
在上面的代碼中,out的內容將是function test(params){\n //在控制台輸出\n console.log('this is a test');\n }
2.可見,通過toString可以得到函數的具體實現,當讓也可得到函數所需要的參數
3.那么我們在調用函數前就可以根據函數中的參數來實現將函數中的參數對象構建出來,傳遞給它。這樣就可以完成依賴注入。
4.但是問題來了。我們知道在js的發布上線的時候,我們為了減小代碼的體積,減輕網絡傳輸的壓力,會代碼進行壓縮處理,壓縮的過程中,會刪除注釋和多余的空格,更高級的壓縮會替換代碼中的變量,將代碼中的變量都替換成最短的變量名。這樣函數依賴的對象名也就被破壞掉了。
5.解決方法:
a.換種方式寫,將依賴用字符串標出,顯試的指明依賴對象:之前的currentTime這樣寫:
angular.module('myService',['ng']);
angular
.module('myService')
.factory('currentTime',['$window',function($window){
return function(){
var now = $window.Date();
return now;
};
}]);
由於代碼中的'$window'是一個字符串常量,在壓縮中是不會被替換的。
b.找angular專用的代碼壓縮工具來壓縮代碼
四、angular中是如何實現依賴注入的
依賴注入的功能主要由源代碼包中./src/auto/injector.js完成。
先上代碼,直接在代碼中注釋
//處理匿名函數,有參數的函數返回‘function(參數)’
//沒有參數的函數,返回‘fn’
//這一段有大量的正則表達式,請自行查閱,有機會我會寫一篇《高效的正則表達式》的博文。
function anonFn(fn) {
// For anonymous functions, showing at the very least the function signature can help in
// debugging.
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
args = fnText.match(FN_ARGS);
if (args) {
return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')';
}
return 'fn';
}
//這個函數的名字叫“注釋者”,功能卻是返回函數的的參數列表
function annotate(fn, strictDi, name) {
var $inject,
fnText,
argDecl,
last;
if (typeof fn === 'function') { //函數使用傳統的方式定義的,function(xx){xxx}
if (!($inject = fn.$inject)) {
$inject = [];
if (fn.length) {
if (strictDi) {
if (!isString(name) || !name) {
name = fn.name || anonFn(fn);
}
throw $injectorMinErr('strictdi',
'{0} is not using explicit annotation and cannot be invoked in strict mode', name);
}
fnText = fn.toString().replace(STRIP_COMMENTS, ''); //刪除函數中的注釋
argDecl = fnText.match(FN_ARGS); //匹配出函數的函數字符串
forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg) {
arg.replace(FN_ARG, function(all, underscore, name) {
$inject.push(name);
});
});
}
fn.$inject = $inject;
}
} else if (isArray(fn)) { //函數是以數組的方式定義的,['xx',function(xx){...}]
last = fn.length - 1;
assertArgFn(fn[last], 'fn');
$inject = fn.slice(0, last);
} else {
assertArgFn(fn, 'fn', true);
}
return $inject;
}
下面看angular"內部注入器"的代碼:
////////////////////////////////////
// 內部注入器
////////////////////////////////////
//這個函數是一個工廠方法,將生成一個“內部注入器”對象
function createInternalInjector(cache, factory) {
function getService(serviceName, caller) { //根據名字獲取服務,如果服務不存在,嘗試使用factory(serviceName, caller)創建服務
if (cache.hasOwnProperty(serviceName)) {
if (cache[serviceName] === INSTANTIATING) {
throw $injectorMinErr('cdep', 'Circular dependency found: {0}',
serviceName + ' <- ' + path.join(' <- '));
}
return cache[serviceName];
} else {
try {
path.unshift(serviceName);
cache[serviceName] = INSTANTIATING;
return cache[serviceName] = factory(serviceName, caller);
} catch (err) {
if (cache[serviceName] === INSTANTIATING) {
delete cache[serviceName];
}
throw err;
} finally {
path.shift();
}
}
}
function invoke(fn, self, locals, serviceName) { //調用函數
if (typeof locals === 'string') {
serviceName = locals;
locals = null;
}
var args = [],
$inject = createInjector.$$annotate(fn, strictDi, serviceName), //前面講過,這里取出的是函數的參數列表,而參數代表了函數依賴的對象
length, i,
key;
for (i = 0, length = $inject.length; i < length; i++) { //根據函數列表,構建函數所依賴的的服務(對象)
key = $inject[i];
if (typeof key !== 'string') {
throw $injectorMinErr('itkn',
'Incorrect injection token! Expected service name as string, got {0}', key);
}
args.push(
locals && locals.hasOwnProperty(key)
? locals[key]
: getService(key, serviceName)
);
}
if (isArray(fn)) {
fn = fn[length];
}
// http://jsperf.com/angularjs-invoke-apply-vs-switch
// #5388
return fn.apply(self, args); //調用函數,到此完全符合我講的的依賴注入原理
}
//實例化,用戶angular內部實例化controller、service、provider等
function instantiate(Type, locals, serviceName) {
// Check if Type is annotated and use just the given function at n-1 as parameter
// e.g. someModule.factory('greeter', ['$window', function(renamed$window) {}]);
// Object creation: http://jsperf.com/create-constructor/2
var instance = Object.create((isArray(Type) ? Type[Type.length - 1] : Type).prototype || null);
var returnedValue = invoke(Type, instance, locals, serviceName);
return isObject(returnedValue) || isFunction(returnedValue) ? returnedValue : instance;
}
//“內部注入器”對象
return {
invoke: invoke, //1.函數調用方法
instantiate: instantiate, //2.內部實例化方法
get: getService, //3."服務對象"獲取方法
annotate: createInjector.$$annotate, //獲取參數列表的方法
has: function(name) {
return providerCache.hasOwnProperty(name + providerSuffix) || cache.hasOwnProperty(name);
}
};
}
}
createInjector.$$annotate = annotate;
到此,可以理解angular的依賴注入式如何完成的。
上一期:angular源碼分析:angular源代碼的獲取與編譯環境安裝
下一期:angular源碼分析:angular中各種常用函數,比較省代碼的各種小技巧