angular 依赖注入原理
依赖注入(Dependency Injection,简称DI)是像C#,java等典型的面向对象语言框架设计原则控制反转的一种典型的一种实现方式,angular把它引入到js中,介绍angular依赖注入的使用方式的文章很多,
angular官方的文档,也有很详细的说明。但介绍原理的较少,angular代码结构较复杂,文章实现了一简化版本的DI,核心代码只有30行左右,相看实现效果(可能需FQ)或查看源码
这篇文章用尽量简单的方式说一说 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]
就可以直接拿到对象。所以只要用一个对象保存对象或函数列表就可以了
1 function createInjector(cache) { 2 this.cache = cache; 3 4 } 5 angular.module = function () { 6 modules = {}; 7 injector = new createInjector(modules); 8 return { 9 injector: injector, 10 factory: function (name, fn) { 11 modules[name.trim()] = this.injector.invoke(fn); 12 return this; 13 } 14 } 15 };
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); } };
简化的全部代码和执行效果见(可能需FQ):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
1 function getService(serviceName, caller) { 2 if (cache.hasOwnProperty(serviceName)) { 3 if (cache[serviceName] === INSTANTIATING) { 4 throw $injectorMinErr('cdep', 'Circular dependency found: {0}', 5 serviceName + ' <- ' + path.join(' <- ')); 6 } 7 return cache[serviceName]; 8 } else { 9 try { 10 path.unshift(serviceName); 11 cache[serviceName] = INSTANTIATING; 12 return cache[serviceName] = factory(serviceName, caller); 13 } catch (err) { 14 if (cache[serviceName] === INSTANTIATING) { 15 delete cache[serviceName]; 16 } 17 throw err; 18 } finally { 19 path.shift(); 20 } 21 } 22 }
3. 执行时注入
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L831
得到参数:
1 function injectionArgs(fn, locals, serviceName) { 2 var args = [], 3 $inject = createInjector.$$annotate(fn, strictDi, serviceName); 4 5 for (var i = 0, length = $inject.length; i < length; i++) { 6 var key = $inject[i]; 7 if (typeof key !== 'string') { 8 throw $injectorMinErr('itkn', 9 'Incorrect injection token! Expected service name as string, got {0}', key); 10 } 11 args.push(locals && locals.hasOwnProperty(key) ? locals[key] : 12 getService(key, serviceName)); 13 } 14 return args; 15 }
调用
https://github.com/angular/angular.js/blob/master/src%2Fauto%2Finjector.js#L861
1 function invoke(fn, self, locals, serviceName) { 2 if (typeof locals === 'string') { 3 serviceName = locals; 4 locals = null; 5 } 6 7 var args = injectionArgs(fn, locals, serviceName); 8 if (isArray(fn)) { 9 fn = fn[fn.length - 1]; 10 } 11 12 if (!isClass(fn)) { 13 // http://jsperf.com/angularjs-invoke-apply-vs-switch 14 // #5388 15 return fn.apply(self, args); 16 } else { 17 args.unshift(null); 18 return new (Function.prototype.bind.apply(fn, args))(); 19 } 20 }
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