MyException - 我的异常网
当前位置:我的异常网» JavaScript » 【 js 基础 】【 源码学习 】backbone 源码翻阅(二

【 js 基础 】【 源码学习 】backbone 源码翻阅(二)

www.MyException.Cn  网友分享于:2013-08-14  浏览:0次
【 js 基础 】【 源码学习 】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 JavaScript

http://yanhaijing.com/program/2016/09/01/about-coupling/    图解7种耦合关系 (推荐大家阅读一下具体的有几种耦合方式)

 

文章评论

程序员眼里IE浏览器是什么样的
程序员眼里IE浏览器是什么样的
团队中“技术大拿”并非越多越好
团队中“技术大拿”并非越多越好
程序员和编码员之间的区别
程序员和编码员之间的区别
程序猿的崛起——Growth Hacker
程序猿的崛起——Growth Hacker
十大编程算法助程序员走上高手之路
十大编程算法助程序员走上高手之路
“懒”出效率是程序员的美德
“懒”出效率是程序员的美德
程序员最害怕的5件事 你中招了吗?
程序员最害怕的5件事 你中招了吗?
Java 与 .NET 的平台发展之争
Java 与 .NET 的平台发展之争
Java程序员必看电影
Java程序员必看电影
我是如何打败拖延症的
我是如何打败拖延症的
要嫁就嫁程序猿—钱多话少死的早
要嫁就嫁程序猿—钱多话少死的早
 程序员的样子
程序员的样子
“肮脏的”IT工作排行榜
“肮脏的”IT工作排行榜
程序员周末都喜欢做什么?
程序员周末都喜欢做什么?
60个开发者不容错过的免费资源库
60个开发者不容错过的免费资源库
什么才是优秀的用户界面设计
什么才是优秀的用户界面设计
我的丈夫是个程序员
我的丈夫是个程序员
Google伦敦新总部 犹如星级庄园
Google伦敦新总部 犹如星级庄园
看13位CEO、创始人和高管如何提高工作效率
看13位CEO、创始人和高管如何提高工作效率
中美印日四国程序员比较
中美印日四国程序员比较
科技史上最臭名昭著的13大罪犯
科技史上最臭名昭著的13大罪犯
为什么程序员都是夜猫子
为什么程序员都是夜猫子
2013年中国软件开发者薪资调查报告
2013年中国软件开发者薪资调查报告
那些争议最大的编程观点
那些争议最大的编程观点
老美怎么看待阿里赴美上市
老美怎么看待阿里赴美上市
我跳槽是因为他们的显示器更大
我跳槽是因为他们的显示器更大
编程语言是女人
编程语言是女人
2013年美国开发者薪资调查报告
2013年美国开发者薪资调查报告
当下全球最炙手可热的八位少年创业者
当下全球最炙手可热的八位少年创业者
程序员都该阅读的书
程序员都该阅读的书
初级 vs 高级开发者 哪个性价比更高?
初级 vs 高级开发者 哪个性价比更高?
写给自己也写给你 自己到底该何去何从
写给自己也写给你 自己到底该何去何从
程序员必看的十大电影
程序员必看的十大电影
如何成为一名黑客
如何成为一名黑客
5款最佳正则表达式编辑调试器
5款最佳正则表达式编辑调试器
漫画:程序员的工作
漫画:程序员的工作
鲜为人知的编程真相
鲜为人知的编程真相
老程序员的下场
老程序员的下场
不懂技术不要对懂技术的人说这很容易实现
不懂技术不要对懂技术的人说这很容易实现
代码女神横空出世
代码女神横空出世
为啥Android手机总会越用越慢?
为啥Android手机总会越用越慢?
旅行,写作,编程
旅行,写作,编程
一个程序员的时间管理
一个程序员的时间管理
聊聊HTTPS和SSL/TLS协议
聊聊HTTPS和SSL/TLS协议
Web开发人员为什么越来越懒了?
Web开发人员为什么越来越懒了?
总结2014中国互联网十大段子
总结2014中国互联网十大段子
程序员应该关注的一些事儿
程序员应该关注的一些事儿
程序员的一天:一寸光阴一寸金
程序员的一天:一寸光阴一寸金
亲爱的项目经理,我恨你
亲爱的项目经理,我恨你
软件开发程序错误异常ExceptionCopyright © 2009-2015 MyException 版权所有