最开始使用angular的时候,总是觉得它的依赖注入方式非常神奇。
如果你跳槽的时候对新公司说,我曾经使用过angular,那他们肯定会问你angular的依赖注入原理是什么?
这篇博客其实是angular源码阅读之路的一个必经站点,就是要理解injector,provider,module之间的关系——这关系其实就是依赖注入的本质。
那么请专注地看下面这一段话吧:
通俗一点的理解:
module是发布任务的BOSS。
injector是领取任务的中间人。
provider是真正去执行任务的马仔。
当然上面这一段话只是比喻,不太严谨,可是很形象。待我慢慢解释来。
如果你比较熟悉angular,那么你肯定知道在每一个module对象上,都有一个私有属性"_invokeQueue"。
这个_invokeQueue,其实就是module发布的任务。
怎么理解『_invokeQueue,其实就是module发布的任务。』这句话呢?请看下面的简单小代码。
当我执行下面这段语句,我会在myapp中创建一个全局变量name='不咬人的蚊子':
//注册了一个全局变量name='不咬人的蚊子' angular.module('myapp').constant('name','不咬人的蚊子');
而这个变量'name'我可以在controller里面这样使用:
angular.module.controller('myctr',['$scope','name',function($scope,name){ console.log(name)//不咬人的蚊子 $scope.name = name; }])
现在说回_invokeQueue,当我执行了那个注册全局变量的constant方法的时候,其实是module发布了一个任务,这个任务保存在_invokeQueue里面。
注意:其实这时候只是发布任务,任务并没有被执行。这时候_invokeQueue里面是这样的:
module._invokeQueue=[ ['constant',['name','不咬人的蚊子']]//数组里面包含着另一个数组。 ]
对,没错,这就是Module发布的任务,invokeQueue其实就是一个数组,里面有着一系列任务(这里只是拿constant举例,其实在真实案例中,还会有各种任务,比如controller啊什么的)。
invokeQueue这个数组里面的每一个元素都是一个任务,如你所见,这任务也是一个数组。
任务数组的第1个元素(下标为0)记录了这个任务具体是什么任务,是constant,还是controller,还是directive等等。
任务数组的第2个元素(下标为1)记录了执行任务需要的参数。
注意注意,这里我们为了易于理解,只拿constant举例子,以后慢慢复杂起来,会越来越丰富。
注意注意,module发布了任务以后,只是发布了,并没有执行。
那么什么时候执行呢?
当angular一个app启动的时候,会自动生成一个injector,也就是大家口中的注射器,这是一个对象,这个injector对象会读取module中的各种任务。
比如injecotr读取module的invokeQueue之后,发现了第一条任务:
['constant',['name','不咬人的蚊子']]
于是injector就会发现,这是一个constant任务,参数是name,'不咬人的蚊子'。
injector并不能处理constant任务,所以它去找一个名为constant的provider,这个provider可以提供一个函数,这个函数正好接收两个参数。
于是injector把任务中的两个参数(也就是name和'不咬人的蚊子'这两个参数)交给constantProvider,让它来执行。
好了,这就是一个口头能讲明白的原理。那么angularJs是如何实现这个机制的呢?我打算把简单版的代码贴在下面,如果感兴趣的同学可以看看,如果不感兴趣的同学其实只要把上面的文字给看明白了,下面的代码随便看个乐呵就行。(这个代码可能会有部分是接着上一篇博客的代码,如果看着不知道怎么回事,可以看看上一篇博客。)
setupModuleLoader.js
function setupModuleLoader(window){ var ensure=function(obj,name,factory){ return obj[name]||(obj[name]=factory()) } var angular = ensure(window,'angular',Object); var createModule = function(name,requires){ var invokeQueue=[];//增加一个任务队列 var moduleInstance = { name:name, requires:requires, _invokeQueue:invokeQueue, //constant方法的实质是向invokeQueue数组里面增加一个任务 constant:function(key,value){ invokeQueue.push(['constant',[key,value]]) }, }; return moduleInstance; } ensure(angular,'module',function(){ var modules={}; return function(name,requires){ if(requires){ return createModule(name,requires,modules) }else{ return getModule(name,modules); } } }) }
createInjector.js
//createInjector(['app1','app2']) //参数是一个字符串或者一个数组,内容是module名 function createInjector(modulesToLoad){ //cache用来缓存一些一直可以用到的值 var cache={}; $provide={ constant:function(key,value){ cache[key]=value; } } //这里的foreach方法并不是一个真正能运行的foreach,能看懂就行了 //每次APP启动的时候,injector都会按照传入的module名来遍历所有module //这样就可以得到所有module发布的任务,并且一一执行这些任务 $.forEach(modulesToLoad,function(moduleName){ var module = window.angular.module(moduleName); $.forEach(module._invokeQueue,function(invokeArgs){ var method=invokeArgs[0]; var args = invokeArgs[1]; $provide[method].apply($provide,args); }) }) return { has:function(key){ return cache.hasOwnProperty(key) }, get:function(key){ return cache[key] } } }
如果你耐着性子看到了这里,并且思路还算清晰,那么你肯定会问,现在injector执行了所有任务,并且把一切东西都准备好了,那么我们使用这些数据的途径和方法是什么呢?哈哈,这个别急,很快会解释明白,但是现在起码我们对依赖注入有了一个很好的理解了不是么?冬天来了,春天不会远了。