依賴注入(Dependency Injection,簡稱DI)是像C#,java等典型的面向對象語言框架設計原則控制反轉的一種典型的一種實現方式,angular把它引入到js中,介紹angular依賴注入的使用方式的文章很多,
angular官方的文檔,也有很詳細的說明。但介紹原理的較少,angular代碼結構較復雜,文章實現了一簡化版本的DI,核心代碼只有30行左右,相看實現效果(可能需翻牆)或查看源碼
這篇文章用盡量簡單的方式說一說 angular依賴注入的實現。
簡化的實現原理
要實現注入,基本有三步:
- 得到模塊的依賴項
- 查找依賴項所對應的對象
- 執行時注入
1. 得到模塊的依賴項
javascript 實現DI的核心api是Function.prototype.toString
,對一個函數執行toString,它會返回函數的源碼字符串,這樣我們就可以通過正則匹配的方式拿到這個函數的參數列表:
function extractArgs(fn) { //angular 這里還加了注釋、箭頭函數的處理
var args = fn.toString().match(/^[^\(]*\(\s*([^\)]*)\)/m);
return args[1].split(',');
}
2. 查找依賴項所對應的對象
java與.net通過反射來獲取依賴對象,js是動態語言,直接一個object[name]
就可以直接拿到對象。所以只要用一個對象保存對象或函數列表就可以了
function createInjector(cache) {
this.cache = cache;
}
angular.module = function () {
modules = {};
injector = new createInjector(modules);
return {
injector: injector,
factory: function (name, fn) {
modules[name.trim()] = this.injector.invoke(fn);
return this;
}
}
};
3. 執行時注入
最后通過 fn.apply方法把執行上下文,和依賴列表傳入函數並執行:
createInjector.prototype = {
invoke: function (fn, self) {
argsString = extractArgs(fn);
args = [];
argsString.forEach(function (val) {
args.push(this.cache[val.trim()]);
}, this);
return fn.apply(self, args);
}
};
簡化的全部代碼和執行效果見(可能需翻牆):http://plnkr.co/edit/sJiIbzEXiqLLoQPeXBnR?p=preview
或查看源碼
這里是簡化的版本,實際angular的實現考慮了很多問題,如模塊管理,延遲執行等
angular 的實現
為了簡單,我們也按這三步來介紹angular DI
- 得到模塊的依賴項
- 查找依賴項所對應的對象
- 執行時注入
注:以下代碼行數有就可能變
1. 得到模塊的依賴項
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L81
var ARROW_ARG = /^([^\(]+?)=>/;
var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
function extractArgs(fn) {
var fnText = fn.toString().replace(STRIP_COMMENTS, ''),
args = fnText.match(ARROW_ARG) || fnText.match(FN_ARGS);
return args;
}
2. 查找依賴項所對應的對象
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L807
function getService(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();
}
}
}
3. 執行時注入
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L831
得到參數:
function injectionArgs(fn, locals, serviceName) {
var args = [],
$inject = createInjector.$$annotate(fn, strictDi, serviceName);
for (var i = 0, length = $inject.length; i < length; i++) {
var 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));
}
return args;
}
調用
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L861
function invoke(fn, self, locals, serviceName) {
if (typeof locals === 'string') {
serviceName = locals;
locals = null;
}
var args = injectionArgs(fn, locals, serviceName);
if (isArray(fn)) {
fn = fn[fn.length - 1];
}
if (!isClass(fn)) {
// http://jsperf.com/angularjs-invoke-apply-vs-switch
// #5388
return fn.apply(self, args);
} else {
args.unshift(null);
return new (Function.prototype.bind.apply(fn, args))();
}
}
angular模塊管理,深坑
angular在每次應用啟動時,初始化一個Injector實例:
https://github.com/angular/angular.js/blob/master/src/Angular.js#L1685
var injector = createInjector(modules, config.strictDi);
由此代碼可以看出對每一個Angular應用來說,無論是哪個模塊,所有的"provider"都是存在相同的providerCache或cache中
所以會導致一個被譽為angular模塊管理的坑王的問題:
module 並沒有什么命名空間的作用,當依賴名相同的時候,后面引用的會覆蓋前面引用的模塊。
具體的示例可以查看:
http://plnkr.co/edit/TZ7hpMwuxk0surlcWDvU?p=preview
注:angular di用本文的調用方式壓縮代碼會出問題:可以用g-annotate轉為安全的調用方式。
到此angular di的實現原理已完成簡單的介紹,angular用了項目中幾乎不會用到的api:Function.prototype.toString 實現依賴注入,思路比較簡單,但實際框架中考慮的問題較多,更加詳細的實現可以直接看angular的源碼。
以后會逐步介紹angular其它原理。
轉載時請注明源出處: http://www.cnblogs.com/etoah/p/5460441.html