最近看完了 backbone.js 的源碼,這裡對於源碼的細節就不再贅述了,大家可以 star 我的源碼閱讀項目(https://github.com/JiayiLi/source-code-study)進行參考交流,有詳細的源碼註釋,以及知識總結,同時 google 一下 backbone 源碼, ...
最近看完了 backbone.js 的源碼,這裡對於源碼的細節就不再贅述了,大家可以 star 我的源碼閱讀項目(https://github.com/JiayiLi/source-code-study)進行參考交流,有詳細的源碼註釋,以及知識總結,同時 google 一下 backbone 源碼,也有很多優秀的文章可以用來學習。
我這裡主要記錄一些偏設計方向的知識點。這篇文章主要講 控制反轉。
一、控制反轉
上篇文章有說到控制反轉,但只是簡略的舉了個例子,在這裡我們詳細說一下這個知識點,它其實並沒有那麼簡單。
控制反轉(Inversion of Control,縮寫為IoC),是面向對象編程中的一種設計原則,可以用來減低電腦代碼之間的耦合度。其中最常見的方式叫做依賴註入(Dependency Injection,簡稱DI),還有一種方式叫“依賴查找”(Dependency Lookup)。通過控制反轉,對象在被創建的時候,由一個調控系統內所有對象的外界實體,將其所依賴的對象的引用傳遞給它。也可以說,依賴被註入到對象中。 -----------來自 wiki (https://zh.wikipedia.org/wiki/%E6%8E%A7%E5%88%B6%E5%8F%8D%E8%BD%AC)
圍繞著概念來學習一下:
首先來解釋一下什麼是耦合度,這樣才能知道 控制反轉到底解決了什麼問題。
耦合度:指一程式中,模塊及模塊之間信息或參數依賴的程度。
舉個例子:
一個程式有20個函數,當你改動其中 1 個函數的時候,其它 19 個函數都需要修改,這就是高耦合,顯然不是我們希望的。
再舉個例子:
在採用面向對象的設計中,程式的實現都是由 n 個對象組成的,這些對象通過彼此的合作,最終實現業務邏輯。就像下麵這個圖:
類似於機械手錶,齒輪之間互相帶動,互相影響,在這種方式的協同工作中,若一個齒輪出現問題不轉了,那麼其他齒輪也會受到影響停止轉動。
對象之間的耦合關係是無法避免的,因為他們要互相配合才能完成工作,當程式功能越來越龐大,對象之間的依賴關係也就越複雜,會出現對象之間的多重依賴關係,就像下麵這個圖,關係是錯綜複雜的:
這個時候如果一個對象的改變,需要和其相關的所有對象都作出改變,牽一發而動全身,一是關係不好理清,二是工作量加大,三是模塊的可復用性低。
為瞭解決這一問題,降低對象模塊之間的低耦合,控制反轉(IoC)理論誕生了。
這個理論希望我們把複雜的功能需求,業務邏輯,拆分成相互合作的對象,這些對象通過封裝以後,可以更加靈活地被重用和擴展,然後藉助“第三方”實現具有依賴關係,但是又是低耦合的合作方式:
通過“第三方”,即 IoC 容器,對象之間的耦合明顯降低,各個齒輪的轉動都是依靠 “第三方”,所有對象的控制權也都是 “第三方” IoC 容器 來管理。正是 IoC 容器把所有對象粘合在一起發揮作用,如果沒有它,對象與對象之間彼此會失去聯繫。
咱們來比較一下 有無引入 IoC 容器 的區別:
A、對於沒有引入 IoC 容器的設計來說,就像第一張圖
Object A 依賴於 Object B,當 Object A 在初始化或者運行到某一點需要 Object B 支持的時候,Object A 必須主動去創建 Object B 或者使用已經創建的 Object B。無論是創建還是使用已經創建了的 Object B,控制權都在 Object A 自己手上。
B、而對於引入 IoC 容器的設計來說,就像第三張圖
由於 IoC 容器 的加入,Object A 與 Object B 之間失去了直接聯繫,當 Object A 運行到需要 Object B 的時候,IoC 容器 會主動創建一個 Object B 註入到 Object A 需要的地方。
通過比較可以看出來,Object A 獲得依賴 Object B 的過程,由主動行為變為了被動行為,控制權顛倒過來了,這也就是 控制反轉 ,反轉的是獲得依賴對象的過程。
那麼到底具體是通過什麼方法來實現控制反轉,降低耦合度的呢,這個 IoC 到底是什麼呢?
這裡就要提到概念里出現的兩種實現 IoC 的方式:依賴註入(Dependency Injection,簡稱DI)和 依賴查找(Dependency Lookup)。
1、依賴註入(DI):就是由 IoC 容器 在運行期間,動態地將某種依賴關係註入到對象之中。類似於一個對象製造工廠,你需要什麼,它會給你送去,你直接使用就行了,而再也不用去關心你所用的東西是如何製成的,也不用關心最後是怎麼被銷毀的,這一切全部由IOC容器包辦。
來舉個例子來看看技術上的實現:例子來自(http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript)
假設我們有兩個模塊。第一個是使Ajax請求的服務,第二個是路由器。我們還有另一個需要這些模塊的功能 doSomething,當然它也可以接受額外的參數來使用其他模塊。
1 var service = function() { 2 return { name: 'Service' }; 3 } 4 var router = function() { 5 return { name: 'Router' }; 6 } 7 var doSomething = function(service,router,other) { 8 var s = service(); 9 var r = router(); 10 };
想象一下如果我們的 doSomething 方法散落在我們的代碼中,這時我們需要更改它的依賴條件,我們需要更改所有調用這個函數的地方。
我們把上面的代碼改成 依賴註入 的方式:
A、RequireJS / AMD 的方法:( 關於 RequireJS / AMD、模塊化的知識,大家可以看我的另一篇文章 http://www.cnblogs.com/lijiayi/p/js_node_module.html )
1 define(['service', 'router'], function(service, router) { 2 // …… 3 });
RequireJS 的 define 方法先描述模塊所需要的依賴,然後再寫模塊的要實現的函數方法。非常好的 依賴註入 的實現。
我們來簡單實現一下 RequireJS / AMD 依賴註入的方法,命名為 injector :
1 var injector = { 2 dependencies: {}, 3 register: function(key, value) { 4 this.dependencies[key] = value; 5 }, 6 resolve: function(deps, func, scope) { 7 var args = []; 8 for (var i = 0; i < deps.length, d = deps[i]; i++) { 9 if (this.dependencies[d]) { 10 args.push(this.dependencies[d]); 11 } else { 12 throw new Error('Can\'t resolve ' + d); 13 } 14 } 15 return function() { 16 func.apply(scope || {}, 17 args.concat(Array.prototype.slice.call(arguments, 0))); 18 } 19 } 20 }
這是一個非常簡單的對象,有兩個方法。register 方法用來註冊所有可以依賴的模塊 。resolve 用來將模塊所需依賴在註冊過的依賴列表dependencies變數中找到並將找到的依賴傳入到 func 參數中。其中依賴的順序不能打亂。
injector的使用:
1 var doSomething = injector.resolve(['service', 'router'], function(service, router, other) { 2 console.log(service().name) // ‘Service' 3 console.log(router().name) // 'Router' 4 console.log(other) // 'Other' 5 }); 6 doSomething("Other");
B、反射方法(angular 實現依賴註入的方法)
反射:在電腦科學中,反射是指電腦程式在運行時(Run time)可以訪問、檢測和修改它本身狀態或行為的一種能力。 -------------來自wiki
在 JavaScript 中,具體指讀取和分析的對象或函數的源代碼。我們可以通過分析代碼,來獲取函數所需要的依賴,然後進行註入。這裡我們就需要使用到 toString() 方法。
當我們調用 doSomething.tostring() 你會得到如下:
1 "function (service, router, other) { 2 var s = service(); 3 var r = router(); 4 }"
這樣我們就可以遍歷這個字元串,得到其需要的參數,也就是所需要的依賴。
我們重新修改一下 上面 injector 方法,主要變化在 resolve 方法上:
1 var injector = { 2 dependencies: {}, 3 register: function(key, value) { 4 this.dependencies[key] = value; 5 }, 6 resolve: function() { 7 var func, deps, scope, args = [], 8 self = this; 9 func = arguments[0]; 10 11 // 這裡的正則幫我們提取出所需要的依賴,正則匹配結果 ["function (service, router, other)", "service, router, other"] 12 deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',’); 13 scope = arguments[1] || {}; 14 return function() { 15 var a = Array.prototype.slice.call(arguments, 0); 16 // 遍歷dependencies數組,如果發現缺失項則嘗試從arguments對象中獲取 17 for (var i = 0; i < deps.length; i++) { 18 var d = deps[i]; 19 args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift()); 20 } 21 func.apply(scope || {}, 22 args); 23 } 24 } 25 }
新版的 injector 的使用:
1 var doSomething = injector.resolve(function(service, other, router) { 2 console.log(service().name) // ‘Service' 3 console.log(router().name) // 'Router' 4 console.log(other) // ‘Other' 5 }); 6 doSomething("Other");
與第一個的方式的區別 :只有一個參數(第一種方法有兩個參數,需要依賴數組),依賴的順序可以打亂。
也證實因為這兩個區別導致這個方法有個問題,當你壓縮了代碼之後,就會改變參數的名字,這樣就不能夠保證 正確的映射關係。例如 doSometing()壓縮後可能看起來像這樣:
1 var doSomething=function(e,t,n){var r=e();var i=t()}
Angular團隊提出的解決方案,傳入這樣形式的參數:
1 var doSomething = injector.resolve(['service', 'router', function(service, router) { 2 3 }]);
我們結合第一種和第二種方案,修改一下 injector 方法 :
1 var injector = { 2 dependencies: {}, 3 register: function(key, value) { 4 this.dependencies[key] = value; 5 }, 6 resolve: function() { 7 var func, deps, scope, args = [], self = this; 8 if(typeof arguments[0] === 'string') { 9 func = arguments[1]; 10 deps = arguments[0].replace(/ /g, '').split(','); 11 scope = arguments[2] || {}; 12 } else { 13 func = arguments[0]; 14 deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(','); 15 scope = arguments[1] || {}; 16 } 17 return function() { 18 var a = Array.prototype.slice.call(arguments, 0); 19 for(var i=0; i<deps.length; i++) { 20 var d = deps[i]; 21 args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift()); 22 } 23 func.apply(scope || {}, args); 24 } 25 } 26 }
新版的 injector 的使用:
1 var doSomething = injector.resolve('router,,service', function(a, b, c) { 2 console.log(a().name) //'Router’ 3 console.log(b) //'Other’ 4 console.log(c().name) //'Service' 5 }); 6 doSomething("Other");
C、直接註入Scope
上面代碼認真看的童鞋會發現,我們的 resolve 方法是有一個參數叫 scope,這其實就是當前作用域,也就是通常意義上的 this 對象。我們可以將依賴綁定到 this 對象上,實現註入。
1 var injector = { 2 dependencies: {}, 3 register: function(key, value) { 4 this.dependencies[key] = value; 5 }, 6 resolve: function(deps, func, scope) { 7 var args = []; 8 scope = scope || {}; 9 for(var i=0; i<deps.length, d=deps[i]; i++) { 10 if(this.dependencies[d]) { 11 scope[d] = this.dependencies[d]; 12 } else { 13 throw new Error('Can\'t resolve ' + d); 14 } 15 } 16 return function() { 17 func.apply(scope || {}, Array.prototype.slice.call(arguments, 0)); 18 } 19 } 20 }
新版的 injector 的使用:
1 var doSomething = injector.resolve(['service', 'router'], function(other) { 2 console.log(this.service().name) // ‘Service' 3 console.log(this.router().name) // 'Router' 4 console.log(other) // ‘Other’ 5 }); 6 doSomething("Other");
2、依賴查找:模塊 利用 IoC 容器提供的回調介面和上下文條件 來找到依賴。
這種情況下模塊就必須使用容器提供的API來查找資源和協作對象,僅有的控制反轉體現在回調方法上:容器將調用回調方法,從而讓模塊獲得所需要的依賴。
對於依賴註入和依賴查找來說,兩者的區別在於:前者是被動的接收對象,在類A的實例創建過程中即創建了依賴的B對象,通過類型或名稱來判斷將不同的對象註入到不同的屬性中,而後者是主動索取相應類型的對象,獲得依賴對象的時間也可以在代碼中自由控制。
依賴查找 相對於 依賴註入來說,用到的比較少,這裡不再詳細講解,大家瞭解一下還有這種方式就可以。
以上,在上篇關於 backbone 的知識總結文章中,我們有提到 backbone 用到了控制反轉,在events.on和events.listenTo 以及 events.once和events.listenToOnce,但其實他只是用到了很小的方面,只是思想的符合,而真正意義上的控制反轉則大面積的運用到了依賴管理中,通過這篇文章,你應該可以有個系統的認識了。
學習並感謝:
https://my.oschina.net/1pei/blog/492601 控制反轉IOC與依賴註入DI http://krasimirtsonev.com/blog/article/Dependency-injection-in-JavaScript Dependency injection in JavaScripthttp://yanhaijing.com/program/2016/09/01/about-coupling/ 圖解7種耦合關係 (推薦大家閱讀一下具體的有幾種耦合方式)