依賴註入起源於實現控制反轉的典型框架Spring框架,用來削減電腦程式的耦合問題。簡單來說,在定義方法的時候,方法所依賴的對象就被隱性的註入到該方法中,在方法中可以直接使用,而不需要在執行該函數的時候再參數中添加這些依賴對象。 理解很簡單,我們以一個例子說明 var $name = "chua",
依賴註入起源於實現控制反轉的典型框架Spring框架,用來削減電腦程式的耦合問題。簡單來說,在定義方法的時候,方法所依賴的對象就被隱性的註入到該方法中,在方法中可以直接使用,而不需要在執行該函數的時候再參數中添加這些依賴對象。
理解很簡單,我們以一個例子說明
var $name = "chua",$age = 26; function myInfo($name,$age){ alert($name + ":" + $age ); }; //普通情況下應該執行 myInfo($name,$age);//"chua:26" //實現依賴註入以後 myInfo();//本人希望列印的結果是"chua:26",但是毫無疑問在沒有實現依賴註入之前是不好使的
那麼怎麼樣實現依賴註入呢?
首先,需要有一個函數來接管myInfo的函數定義,不然我們沒法拿到myInfo的依賴對象名稱加以保存。
function injector(fn){ //處理fn,保存依賴對象 ... return function(){ fn.apply({},xxx);//xxx是處理過後的參數 } } //myInfo的定義變成 var myInfo = injector(function ($name,$age){ alert($name + ":" + $age ); }); myInfo();
injector函數是怎麼獲取到myInfo的依賴對象$name、$age的?我們可以通過傳入參數的函數fn.toString()來看,
//fn.toString()的結果為下麵的字元串 "function myInfo($name,$age){ alert($name + ":" + $age ); }"
參考angular的處理:從字元串中讀取函數myInfo的參數還是能讀取。
STRIP_COMMENTS =/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm, FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m, FN_ARG =/^\s*(\S+?)\s*$/; function injector(fn){ //處理fn,保存依賴對象 var fnText = fn.toString().replace(STRIP_COMMENTS, '');//"function myInfo($name,$age){ alert($name + ":" + $age ); }" var argDecl = fnText.match(FN_ARGS);//["function myInfo($name,$age)", "$name,$age"] var args = argDecl[1].split(",");//["$name","$age"] for(var i = 0; i < args.length; i++){ args[i] = args[i].replace(FN_ARG,"$1"); } ...
return function(){ fn.apply({},xxx);//xxx是處理過後的參數 } }
但是有這個還不夠,我們雖然拿到了要依賴的對象名稱,但是我們並沒有給這些名稱指定對應的對象。所以應當有一個給這些要依賴的對象註冊的過程。
function injector(fn){ ... var args = argDecl[1].split(","); for(var i = 0; i < args.length; i++){ args[i] = injector.cache[args[i].replace(FN_ARG,"$1")]; } return function(){ fn.apply({},args); } } injector.cache = {}; injector.register = function(key,resource){ injector.cache[key] = resource; }
//先註冊要依賴的對象 injector.register("$name",$name); injector.register("$age",$age);
這個時候我們可以使用injector來聲明函數了
var myInfo = injector(function($name,$age){ alert($name + ":" + $age ); });
//沒有任何參數,得到期望的結果 myInfo();//chua:26
只要是已經註冊過的對象就多可以註入到使用injector函數來聲明的函數中使用。完整的測試代碼如下
<script> var $name = "chua", $age = 26, STRIP_COMMENTS =/((\/\/.*$)|(\/\*[\s\S]*?\*\/))/gm, FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m, FN_ARG =/^\s*(\S+?)\s*$/; function injector(fn){ //處理fn,保存依賴對象 var fnText = fn.toString().replace(STRIP_COMMENTS, '');//"function myInfo($name,$age){ alert($name + ":" + $age ); }" var argDecl = fnText.match(FN_ARGS);//["function myInfo($name,$age)", "$name,$age"] var args = argDecl[1].split(",");//["$name","$age"] for(var i = 0; i < args.length; i++){ args[i] = injector.cache[args[i].replace(FN_ARG,"$1")]; } return function(){ fn.apply({},args); } } injector.cache = {}; injector.register = function(key,resource){ injector.cache[key] = resource; } //先註冊要依賴的對象 injector.register("$name",$name); injector.register("$age",$age); //下麵這個奇跡出現的時刻 var myInfo = injector(function($name,$age){ alert($name + ":" + $age ); }); myInfo();//chua:26 </script>View Code
好了簡單的實現了一個依賴註入。但是依賴註入有一個問題,依賴註入是基於依賴對象的名稱字元串的
injector(function($name,$age){...});
那麼上面的代碼通過一般的壓縮工具壓縮以後會變成
a(function(b,c){...});
已經無法找到依賴對象是"$name"/"$age"對應的對象了。
所以angular想了幾個辦法。
angular簡單註入方式為
myModule.controller('myCtrl',function($scope) {... });
數組註釋法
myModule.controller('myCtrl', ['$scope', function($scope) {... }]);
顯式註入法
function myCtrl(){...};
myCtrl.$inject = ["$scope"];
myModule.controller('myCtrl',myCtrl);
有一個第三方的插件ng-min,它可以幫你將以簡單方式註入的代碼自動轉換成數組註釋的方式,即能滿足你寫簡潔代碼的願望,又能避免壓縮出錯問題。ng-min地址:https://github.com/btford/ngmin
angular實現依賴註入的方式
應該說,js的依賴註入實現方式都是類似的。angular實現的依賴註入對象結構如下
injector = { annotate: function annotate(fn, strictDi, name), get: function getService(serviceName, caller), has: function(name), instantiate: function instantiate(Type, locals, serviceName), invoke: function invoke(fn, self, locals, serviceName) }
1.需要有註入的對象(依賴的對象)保存的地方。
不知道還記不記得我的angular源碼學習之旅1中提到angularModule = setupModuleLoader(window)給angular添加了模塊聲明(添加模塊)、載入(獲取模塊)的方法angular.module。而在創建註入對象的時候(createInjector函數中)有這麼一段代碼
forEach(loadModules(modulesToLoad), function(fn) { if (fn) instanceInjector.invoke(fn); });
其中loadModules(modulesToLoad)就是去載入依賴模塊,在loadModules函數中真正去載入模塊的是這段代碼
if (isString(module)) {
moduleFn = angularModule(module);
...
}
angularModule即angular.module。前面也分析過這些模塊實際都保存在一個外部變數modules中
所以通過angular.module方法就是通過模塊名去modules上添加或取出其中的模塊。
2.獲取依賴註入列表(即要依賴的對象的名稱)
獲取方式也是通過fn.toString()來獲取的。angular的實現是injector.annotate,函數體為
function annotate(fn, strictDi, name) { var $inject, fnText, argDecl, last; if (typeof fn === 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { ... 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)) { ... } else { ... } return $inject; }
只是應為angular接受多種註入方式所以處理代碼有多個分支,而且也只是保存了依賴對象名稱,並沒有返回執行函數,執行函數是統一的injector.invoke。
先前我們自己的實現執行是直接調用的myInfo();angular的執行函數是injector.invoke,函數體如下
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]; ...
//獲取依賴對象 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); }
好了angularjs的依賴註入實現分析到此結束。更詳細的細節可以斷點跟蹤。
如果覺得本文不錯,請點擊右下方【推薦】!