1. 程式人生 > >【轉】Javascript DI!Angular依賴注入的實現原理

【轉】Javascript DI!Angular依賴注入的實現原理

DI是Angular的特色功能,而在Angular 2.0的計劃中,DI將成為一個獨立的模組,參見https://github.com/angular/di.js這意味著它也有機會被用於nodejs等技術中,其他前端框架也完全有機會使用它。

DI簡介

假設你需要某個物件,你有幾種選擇?

方法一、自己建立它。

這是看起來最簡單直觀的方法,但這只是看起來很美,因為物件的建立有可能很複雜:需要一系列的引數,可能還需要先這樣、再那樣的一系列初始化操作,甚至可能還需要建立它的相關物件,相關物件還需要建立相關物件的相關物件……你確定你在寫程式碼而不是擇毛線嗎?一句話,這種方式無法適應複雜的物件,隨著物件的複雜化,自己動手豐衣足食已經不再是好的選擇。

方法二、使用者從全域性登錄檔查閱它。

這種方法輕鬆多了,物件的建立者要自己負責建立自己,然後把自己註冊到某個全域性登錄檔中,你需要它的時候,就去登錄檔中根據名字或者其它的獨有資訊(比如強型別語言中的類名)查詢:obj = globalRegistry.get(objectId)。顯然,輕鬆多了,再也不用管它的初始化引數和初始化流程了,拿來就用。那麼,問題在哪裡?問題在於它很難被單元測試。我們都知道,全域性變數是單元測試中的魔鬼,因為它會讓各個“單元”互相耦合在一起,那將是單元測試的噩夢。而globalRegistry就是一個全域性變數,並且連累著其中的物件也都變成全域性的了。更重要的是:你還不夠懶。

方法三、衣來伸手,飯來張口。

這就是所謂DI了。也就是說,我只要指出我需要哪些物件,然後就有人會把這個物件給我,而這個“人”可能是一個應用框架(Framework),也可能是測試容器(Test Runner),我不會關心它是誰,也不用關心它怎麼得到的這個物件。這個“人”,專業的說法叫“容器”,下面我會說到這個容器在Angular中對應哪個概念。

而我指出“我需要哪些物件”的方式,也有多種,比如可以直接宣告一個屬性,或者寫個Annotation,或者寫個配置檔案來宣告依賴關係,或者在函式的引數中宣告,目前angular所採用的方式是函式的引數形式,和一種變形的Annotation形式來防止minify破壞名稱,據說angular 2.0會使用新的語言特性強化annotation的方式。

JavaScript中實現DI的原理

在JavaScript中實現DI,看起來難,實際上原理很簡單,它的核心技術是Function物件的toString()。我們都知道,對一個函式物件執行toString(),它的返回值是函式的原始碼,知道了這一點,接下來就簡單的:我獲取了函式原始碼,然後我對函式的宣告進行解析,偽碼如下:
複製程式碼
var giveMe = function(config) {
};
var registry = {};
var inject = function(func, thisForFunc) {
// 獲取原始碼
var source = func.toString();
// 用正則表示式解析原始碼
var matcher = source.match(/..表示式有些複雜,省略../);
// 解析結果是各個引數的名稱
var objectIds = ....
// 查閱出相應的物件,放到陣列中準備作為引數傳過去
var objects = [];
for (var i = 0; i < objectIds.length; ++i)
objects.push(registry[objectIds[i]]);
// 呼叫這個函式,並且把引數傳過去
func.apply(thisForFunc || func, objects)
};
inject(giveMe)

當然,一個實際的DI系統需要考慮的問題比這要多很多,但是這段程式碼用來表現原理應該足夠了。

Angular中的DI

接下來我們再來看Angular中的DI實現:

在Angular中,所有主要程式設計元素都需要通過某種方式註冊進去,比如myModule.service(‘serviceName’, function()…. 這實際上就是把後面這個函式加入到一個容器中,要注意的是:angular全面實現了延遲初始化,也就是說,當這個物件沒有被別人需要的時候,它是不會被建立的,這樣對於提高效能有一定的幫助,特別是加快了啟動速度。

這裡一個有趣的問題是:Angular的容器是什麼。Angular不存在真正的全域性物件,所以你可以放心的在同一個頁面中放多個app,而不用擔心他們互相干擾,但是容器又需要一個眾所周知的地方來存放這些“名字和物件”的登錄檔(Registry),在Angular中,這個登錄檔就叫做module,所以,現在你應該知道為什麼module的地位很重要了吧?不過一個app中可以存在很多不同名字的module,它們之間存在某些依賴關係,而這體現在module的宣告語法中:angular.module(‘someModule’, [‘dep1’, ‘dep2]),這樣劃分module有利於程式的檔案組織。

根據DI的原理,一個自然的推論就是:被注入的物件通常都是單例,因為建立了一個,就可以始終使用它了,不需要多次建立。因此,如果你需要在angular中跨controller共享資料或者通訊,那麼你可以建立一個service/value/constant等,然後把它們分別注入到兩個controller中,而這兩個controller將自然而然的共享同一個物件。