Ioc - Inversion of Control , 即"控制反轉"。在開發中, IoC 意味着你設計好的對象交給容器控制,而不是使用傳統的方式,在對象內部直接控制。 編程
如何理解好 IoC 呢?理解好 IoC的關鍵是要明確"誰控制誰,控制什麼,爲什麼是反轉(有反轉就應該有正轉),哪些方面反轉了",咱們來深刻分析一下。 數組
誰控制誰,控制什麼: 在傳統的程序設計中,咱們直接在對象內部經過 new 的方式建立對象,是程序主動建立依賴對象;而 IoC 是有專門一個容器來建立這些對象,即由 IoC 容器控制對象的建立;誰控制誰?固然是 IoC 容器控制了對象;控制什麼?主要是控制外部資源獲取。瀏覽器
爲什麼是反轉了,哪些方面反轉了: 有反轉就有正轉,傳統應用程序是由咱們本身在對象中主動控制去獲取依賴對象,也就是正轉;而反轉則是由容器來幫忙建立及注入依賴對象;爲什麼是反轉?由於由容器幫咱們查找及注入依賴對象,對象只是被動的接受依賴對象,因此是反轉了;哪些方面反轉了?依賴對象的獲取被反轉了。緩存
Ioc 不是一種技術,只是一種思想,一個重要的面向對象編程法則,它能指導咱們如何設計鬆耦合、更優良的系統。傳統應用程序都是由咱們在類內部主動建立依賴對象,從而致使類與類之間高耦合,難於測試;有了 IoC 容器後,把建立和查找依賴對象的控制權交給了容器,由容器注入組合對象,因此對象之間是鬆散耦合,這樣也便於測試,利於功能複用,更重要的是使得程序的整個體系結構變得很是靈活。 angular2
其實 IoC 對編程帶來的最大改變不是從代碼上,而是思想上,發生了"主從換位"的變化。應用程序原本是老大,要獲取什麼資源都是主動出擊,但在 IoC思想中,應用程序就變成被動了,被動的等待 IoC 容器來建立並注入它所需的資源了。 框架
DI - Dependency Injection,即"依賴注入":組件之間的依賴關係由容器在運行期決定,形象的說,即由容器動態的將某個依賴關係注入到組件之中。依賴注入的目的並不是爲軟件系統帶來更多功能,而是爲了提高組件重用的頻率,併爲系統搭建一個靈活、可擴展的平臺。經過依賴注入機制,咱們只須要經過簡單的配置,而無需任何代碼就可指定目標須要的資源,完成自身的業務邏輯,而不須要關心具體的資源來自何處,由誰實現。 異步
理解 DI 的關鍵是:"誰依賴了誰,爲何須要依賴,誰注入了誰,注入了什麼",那咱們來深刻分析一下: ide
誰依賴了誰:固然是應用程序依賴 IoC 容器函數
爲何須要依賴:應用程序須要 IoC 容器來提供對象須要的外部資源學習
誰注入誰:很明顯是 IoC 容器注入應用程序依賴的對象
注入了什麼:注入某個對象所需的外部資源(包括對象、資源、常量數據)
IoC 和 DI 有什麼關係?其實它們是同一個概念的不一樣角度描述,因爲控制反轉的概念比較含糊(可能只是理解爲容器控制對象這一個層面,很難讓人想到誰來維護依賴關係),因此 2004 年大師級人物 Martin Fowler 又給出了一個新的名字:"依賴注入",相對 IoC 而言,"依賴注入" 明確描述了被注入對象依賴 IoC 容器配置依賴對象。
總的來講, 控制反轉(Inversion of Control)是說建立對象的控制權發生轉移,之前建立對象的主動權和建立時機由應用程序把控,而如今這種權利轉交給 IoC 容器,它就是一個專門用來建立對象的工廠,你須要什麼對象,它就給你什麼對象。有了 IoC 容器,依賴關係就改變了,原先的依賴關係就沒了,它們都依賴 IoC容器了,經過 IoC 容器來創建它們之間的關係。
angular1 中聲明依賴項的方式有3種,分爲以下:
// 方式一: 使用 $inject annotation 方式 var fn = function (a, b) {}; fn.$inject = ['a', 'b']; // 方式二: 使用 array-style annotations 方式 var fn = ['a', 'b', function (a, b) {}]; // 方式三: 使用隱式聲明方式 var fn = function (a, b) {}; // 不推薦
爲了支持以上多種聲明方式,angular1 內部使用 annotate 函數來解析依賴項,該函數的實現以下:
var FN_ARGS = /^[^\(]*\(\s*([^\)]*)\)/m; // 匹配參數列表 var FN_ARG_SPLIT = /,/; // 參數分隔符 var FN_ARG = /^\s*(_?)(\S+?)\1\s*$/; // 匹配參數項 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; } function anonFn(fn) { var args = extractArgs(fn); if (args) { return 'function(' + (args[1] || '').replace(/[\s\r\n]+/, ' ') + ')'; } return 'fn'; } function annotate(fn, strictDi, name) { var $inject, argDecl, last; if (typeof fn === 'function') { if (!($inject = fn.$inject)) { // 判斷是否使用$inject方式聲明依賴項 $inject = []; if (fn.length) { if (strictDi) { // 使用嚴格注入模式,即不能使用隱式聲明方式 // 函數名非字符串或爲falsy值(如undefined、null),未設置時默認值爲undefined if (!isString(name) || !name) { name = fn.name || anonFn(fn); } throw $injectorMinErr('strictdi', '{0} is not using explicit annotation and cannot be invoked in strict mode', name); } argDecl = extractArgs(fn); // 處理隱式聲明方式 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)) { // 使用 array-style annotations 方式 last = fn.length - 1; // 獲取fn函數 assertArgFn(fn[last], 'fn'); $inject = fn.slice(0, last); // 獲取依賴項 } else { assertArgFn(fn, 'fn', true); } return $inject; // 返回依賴數組 }
angular1 內部經過調用 annotate 函數,獲取函數的依賴列表(即依賴數組)後,應該如何獲取每一個項對應的依賴對象呢?咱們來進一步分析一下:
假設咱們使用 array-style annotations 方式聲明 fn 函數:
var fn = ['a', 'b', function (a, b) {}]
調用annotate函數後,咱們得到 fn 的依賴列表,即返回 ['a','b']。
獲取依賴列表後,咱們就可以根據依賴項的名稱來獲取對應的依賴對象。所以,依賴名與依賴對象的存儲方式應該是使用 Key - Value 的方式進行存儲(在 ES5 中咱們可使用對象字面量,如 var cache = {} 實現 K-V 存儲)。在 angular1 內部提供了一個 getService 方法,用來獲取依賴對象。它的具體實現以下:
var INSTANTIATING = {}, // 是否實例化中 providerSuffix = 'Provider', // provider後綴 path = []; // 依賴路徑 var factory = function(serviceName, caller) { // 實例工廠 var provider = providerInjector.get(serviceName + providerSuffix, caller); return instanceInjector.invoke(provider.$get, provider, undefined, serviceName); }); 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; // 實例化 serviceName 對應的依賴對象並存儲 return cache[serviceName] = factory(serviceName, caller); } catch (err) { if (cache[serviceName] === INSTANTIATING) { delete cache[serviceName]; // 實例化失敗,從緩存中移除 } throw err; } finally { path.shift(); } } }
經過 getService 的實現方式,咱們能夠知道,若依賴對象已存在,咱們直接從緩存中獲取,若是依賴對象不存在,咱們經過調用 serviceName 對象的provider來建立依賴對象,而後保存在對象實例緩存中。這樣的話,間接說明了一個問題,即在 angular1 中,全部的依賴對象都是單例。
這裏咱們先稍微解釋一下Provider,而後再來列舉 angular1 DI系統存在的一些問題。
什麼是Provider ?在 angular1 中,Provider是一個包含 $get 屬性的普通 JS 對象。建立 provider 有兩種方式:
// 方式一: 使用對象方式 module.provider('a',{ $get: function () { return 42; } }); // 方式二: 使用構造函數方式 module.provider('a', function AProvider() { this.$get = function() { return 42; }; });
以上兩種方式都是使用 module 對象提供的provider方法來註冊 provider,angular1 中 provider 的具體實現以下:
function provider(name, provider_) { // provider 的名稱不能爲hasOwnProperty assertNotHasOwnProperty(name, 'service'); // 構造函數方式,先進行實例化 if (isFunction(provider_) || isArray(provider_)) { provider_ = providerInjector.instantiate(provider_); } if (!provider_.$get) { // 判斷 provider_ 對象是否存在 $get屬性 throw $injectorMinErr('pget', "Provider '{0}' must define $get factory method.", name); } // 使用 name + "Provider"做爲 Key 值,保存在 providerCache 中,用於建立實例 return providerCache[name + providerSuffix] = provider_; }
內部緩存: angular1 應用程序中全部的依賴項都是單例,咱們不能控制是否使用新的實例
命名空間衝突: 在系統中咱們使用字符串來標識 service 的名稱,假設咱們在項目中已有一個 CarService,然而第三方庫中也引入了一樣的服務,這樣的話就容易出現混淆
DI 耦合度過高: angular1 中 DI 功能已經被框架集成了,咱們不能單獨使用它的 DI 特性
未能和模塊加載器結合: 在瀏覽器環境中,不少場景都是異步的過程,咱們須要的依賴模塊並非一開始就加載好的,或許咱們在建立的時候纔會去加載依賴模塊,再進行依賴建立,而 angualr 的 IoC 容器無法作到這點。
本文首先介紹了 IoC 和 DI 的概念及做用,而後講述了 DI 在 angular1 中的實際應用。此外,簡單的介紹了, angular1 DI 的實現方式,但並未深刻介紹 angular1 中的 injector ,有興趣的同窗能夠自行了解一下。最後,咱們介紹了 angular1 DI 系統中存在的問題,這樣爲咱們後面學習 angular2 DI 系統作好了鋪墊,咱們能更好地理解它設計的意圖。