【 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种耦合关系 (推荐大家阅读一下具体的有几种耦合方式)

时间: 2024-10-26 04:21:07

【 js 基础 】【 源码学习 】backbone 源码阅读(二)的相关文章

JS基础知识回顾:ECMAScript的语法(二)

ECMAScript中有五种简单数据类型(也称为基本数据类型):Undefined.Null.Boolean.Number.String ECMAScript还有一种复杂数据类型——Object,Object本质上是由一组无序的名值对组成的. ECMAScript不支持任何创建自定义类型的机制,而所有值最终都将是上述六种数据类型之一,由于ECMAScript的数据类型具有动态性,因此的确没有再定义其他数据类型的必要了. 监狱ECMAScript是松散类型的,因此需要有一种手段来检测给定变量的数据

【 js 基础 】【 源码学习 】源码设计 (持续更新)

学习源码,除了学习对一些方法的更加聪明的代码实现,同时也要学习源码的设计,把握整体的架构.(推荐对源码有一定熟悉了之后,再看这篇文章) 目录结构:第一部分:zepto 设计分析第二部分:underscore 设计分析 第一部分: zepto 设计分析zepto 是一个轻量级的 Javascript 库.相对于 jquery 来说在 size 上更加小,主要是定位于移动设备.它是非常好的学习源码的入门级 javascript 库.这里重点说一下,这个库的设计,而对于详细的源码学习大家可以 star

async源码学习 - 全部源码

因为工作需要,可能我离前端走远了,偏node方向了.所以异步编程的需求很多,于是乎,不得不带着学习async了. 我有个习惯,用别人的东西之前,喜欢稍微搞明白点,so就带着看看其源码. github: https://github.com/caolan/async 文档:http://caolan.github.io/async/ 里面提供的工具方法,控制流程方法还是很多的.所以需要哪些方法,就看相应的源码. 下面是其全部源码. (function (global, factory) { typ

Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁

上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列中的每个结点(线程)只要等待其前继释放锁就可以了. AbstractQueuedSynchronizer是通过一个内部类Node来实现CLH lock queue的一个变种,但基本原理是类似的. 在介绍Node类之前,我们来介绍下Spin Lock,通常就是用CLH lock queue来实现自旋锁

Google之Chromium浏览器源码学习——base公共通用库(二)

上次提到Chromium浏览器中base公共通用库中的内存分配器allocator,其中用到了三方库tcmalloc.jemalloc:对于这两个内存分配器,个人建议,对于内存,最好是自己维护内存池:此外在windows下使用可使用其自带的内存分配方式:具体的第三方库可以参阅:http://www.360doc.com/content/13/0915/09/8363527_314549128.shtml:目前我们主要的精力尽可能在Chromium浏览器中base公共通用库的主要内容上,所以不再详

猫猫学iOS之二维码学习,快速打开相机读取二维码

猫猫分享,必须精品 原创文章,欢迎转载.转载请注明:翟乃玉的博客 地址:http://blog.csdn.net/u013357243 上一篇文章写了怎么生成二维码,这儿就说说怎么读取吧,反正也很简单,iOS封装的太强大了 步骤呢就是这样: 读取二维码需要导入AVFoundation框架#import <AVFoundation/AVFoundation.h> 1:利用摄像头识别二维码中的内容(模拟器不行). 2:输入(摄像头). 3:由会话将摄像头采集到的二维码图像转换成字符串数据. 4:输

javascript(js)基础之dom学习

dom学习 <img id='xx'.. onclick='aa()'> functon aa(){ xx1=document.getElementById("xx") //下面对xx1进行操作 } bom介绍:浏览器对象模型 因为浏览器企业太多,w3c定义了一个做浏览器的规范 规定 ----------- dom介绍/学习:文档对象模型 dom树 例子 <script language="JavaScript"> function text

鸟哥的Linux私房菜_基础版_学习笔记8:第十二章 正规表示法与文件格式化处理

11.2 基础正规表示法 11.2.1 语系对正规表示法的影响 由於不同语系的编码数据并不相同,所以就会造成数据撷取结果的差异了. 举例来说,在英文大小写的编码顺序中,zh_TW.big5 及 C 这两种语系的输出结果分别如下: LANG=C     时:0 1 2 3 4 ... A B C D ... Z a b c d ...z LANG=zh_TW 时:0 1 2 3 4 ... a A b B c C d D ... z Z 特殊符号 代表意义 [:alnum:] 代表英文大小写字节及

Java并发包源码学习之AQS框架(一)概述

AQS其实就是java.util.concurrent.locks.AbstractQueuedSynchronizer这个类. 阅读Java的并发包源码你会发现这个类是整个java.util.concurrent的核心之一,也可以说是阅读整个并发包源码的一个突破口. 比如读ReentrantLock的源码你会发现其核心是它的一个内部类Sync: 整个包中很多类的结构都是如此,比如Semaphore,CountDownLatch都有一个内部类Sync,而所有的Sync都是继承自AbstractQ

Spark 消息队列机制源码学习

源码学习 spark源码注释中有下面一句话: Asynchronously passes SparkListenerEvents to registered SparkListeners 即所有spark消息SparkListenerEvents 被异步的发送给已经注册过的SparkListeners. 在SparkContext中, 首先会创建LiveListenerBus实例,这个类主要功能如下: 保存有消息队列,负责消息的缓存 保存有注册过的listener,负责消息的分发 该类的继承层次