最近看完了 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 JavaScript
http://yanhaijing.com/program/2016/09/01/about-coupling/ 图解7种耦合关系 (推荐大家阅读一下具体的有几种耦合方式)