【 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,当然它也可以接受额外的参数来使用其他模块。
var service = function() {
return { name: 'Service' };
}
var router = function() {
return { name: 'Router' };
}
var doSomething = function(service,router,other) {
var s = service();
var r = router();
};
想象一下如果我们的 doSomething 方法散落在我们的代码中,这时我们需要更改它的依赖条件,我们需要更改所有调用这个函数的地方。
我们把上面的代码改成 依赖注入 的方式:
A、RequireJS / AMD 的方法:( 关于 RequireJS / AMD、模块化的知识,大家可以看我的另一篇文章 http://www.cnblogs.com/lijiayi/p/js_node_module.html )
define(['service', 'router'], function(service, router) {
// ……
});
RequireJS 的 define 方法先描述模块所需要的依赖,然后再写模块的要实现的函数方法。非常好的 依赖注入 的实现。
我们来简单实现一下 RequireJS / AMD 依赖注入的方法,命名为 injector :
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
for (var i = 0; i < deps.length, d = deps[i]; i++) {
if (this.dependencies[d]) {
args.push(this.dependencies[d]);
} else {
throw new Error('Can\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {},
args.concat(Array.prototype.slice.call(arguments, 0)));
}
}
}
这是一个非常简单的对象,有两个方法。register 方法用来注册所有可以依赖的模块 。resolve 用来将模块所需依赖在注册过的依赖列表dependencies变量中找到并将找到的依赖传入到 func 参数中。其中依赖的顺序不能打乱。
injector的使用:
var doSomething = injector.resolve(['service', 'router'], function(service, router, other) {
console.log(service().name) // ‘Service'
console.log(router().name) // 'Router'
console.log(other) // 'Other'
});
doSomething("Other");
B、反射方法(angular 实现依赖注入的方法)
反射:在计算机科学中,反射是指计算机程序在运行时(Run time)可以访问、检测和修改它本身状态或行为的一种能力。 -------------来自wiki
在 JavaScript 中,具体指读取和分析的对象或函数的源代码。我们可以通过分析代码,来获取函数所需要的依赖,然后进行注入。这里我们就需要使用到 toString() 方法。
当我们调用 doSomething.tostring() 你会得到如下:
"function (service, router, other) {
var s = service();
var r = router();
}"
这样我们就可以遍历这个字符串,得到其需要的参数,也就是所需要的依赖。
我们重新修改一下 上面 injector 方法,主要变化在 resolve 方法上:
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function() {
var func, deps, scope, args = [],
self = this;
func = arguments[0]; // 这里的正则帮我们提取出所需要的依赖,正则匹配结果 ["function (service, router, other)", "service, router, other"]
deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',’);
scope = arguments[1] || {};
return function() {
var a = Array.prototype.slice.call(arguments, 0);
// 遍历dependencies数组,如果发现缺失项则尝试从arguments对象中获取
for (var i = 0; i < deps.length; i++) {
var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {},
args);
}
}
}
新版的 injector 的使用:
var doSomething = injector.resolve(function(service, other, router) {
console.log(service().name) // ‘Service'
console.log(router().name) // 'Router'
console.log(other) // ‘Other'
});
doSomething("Other");
与第一个的方式的区别 :只有一个参数(第一种方法有两个参数,需要依赖数组),依赖的顺序可以打乱。
也证实因为这两个区别导致这个方法有个问题,当你压缩了代码之后,就会改变参数的名字,这样就不能够保证 正确的映射关系。例如 doSometing()压缩后可能看起来像这样:
var doSomething=function(e,t,n){var r=e();var i=t()}
Angular团队提出的解决方案,传入这样形式的参数:
var doSomething = injector.resolve(['service', 'router', function(service, router) { }]);
我们结合第一种和第二种方案,修改一下 injector 方法 :
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function() {
var func, deps, scope, args = [], self = this;
if(typeof arguments[0] === 'string') {
func = arguments[1];
deps = arguments[0].replace(/ /g, '').split(',');
scope = arguments[2] || {};
} else {
func = arguments[0];
deps = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1].replace(/ /g, '').split(',');
scope = arguments[1] || {};
}
return function() {
var a = Array.prototype.slice.call(arguments, 0);
for(var i=0; i<deps.length; i++) {
var d = deps[i];
args.push(self.dependencies[d] && d != '' ? self.dependencies[d] : a.shift());
}
func.apply(scope || {}, args);
}
}
}
新版的 injector 的使用:
var doSomething = injector.resolve('router,,service', function(a, b, c) {
console.log(a().name) //'Router’
console.log(b) //'Other’
console.log(c().name) //'Service'
});
doSomething("Other");
C、直接注入Scope
上面代码认真看的童鞋会发现,我们的 resolve 方法是有一个参数叫 scope,这其实就是当前作用域,也就是通常意义上的 this 对象。我们可以将依赖绑定到 this 对象上,实现注入。
var injector = {
dependencies: {},
register: function(key, value) {
this.dependencies[key] = value;
},
resolve: function(deps, func, scope) {
var args = [];
scope = scope || {};
for(var i=0; i<deps.length, d=deps[i]; i++) {
if(this.dependencies[d]) {
scope[d] = this.dependencies[d];
} else {
throw new Error('Can\'t resolve ' + d);
}
}
return function() {
func.apply(scope || {}, Array.prototype.slice.call(arguments, 0));
}
}
}
新版的 injector 的使用:
var doSomething = injector.resolve(['service', 'router'], function(other) {
console.log(this.service().name) // ‘Service'
console.log(this.router().name) // 'Router'
console.log(other) // ‘Other’
});
doSomething("Other");
2、依赖查找:模块 利用 IoC 容器提供的回调接口和上下文条件 来找到依赖。
这种情况下模块就必须使用容器提供的API来查找资源和协作对象,仅有的控制反转体现在回调方法上:容器将调用回调方法,从而让模块获得所需要的依赖。
对于依赖注入和依赖查找来说,两者的区别在于:前者是被动的接收对象,在类A的实例创建过程中即创建了依赖的B对象,通过类型或名称来判断将不同的对象注入到不同的属性中,而后者是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制。
依赖查找 相对于 依赖注入来说,用到的比较少,这里不再详细讲解,大家了解一下还有这种方式就可以。
以上,在上篇关于 backbone 的知识总结文章中,我们有提到 backbone 用到了控制反转,在events.on和events.listenTo 以及 events.once和events.listenToOnce,但其实他只是用到了很小的方面,只是思想的符合,而真正意义上的控制反转则大面积的运用到了依赖管理中,通过这篇文章,你应该可以有个系统的认识了。
学习并感谢:
http://yanhaijing.com/program/2016/09/01/about-coupling/ 图解7种耦合关系 (推荐大家阅读一下具体的有几种耦合方式)
【 js 基础 】【 源码学习 】backbone 源码阅读(二)的更多相关文章
- 【 js 基础 】【 源码学习 】源码设计 (更新了backbone分析)
学习源码,除了学习对一些方法的更加聪明的代码实现,同时也要学习源码的设计,把握整体的架构.(推荐对源码有一定熟悉了之后,再看这篇文章) 目录结构:第一部分:zepto 设计分析 第二部分:unders ...
- 【 js 基础 】【 源码学习 】源码设计 (持续更新)
学习源码,除了学习对一些方法的更加聪明的代码实现,同时也要学习源码的设计,把握整体的架构.(推荐对源码有一定熟悉了之后,再看这篇文章) 目录结构:第一部分:zepto 设计分析第二部分:undersc ...
- Vue2.x源码学习笔记-源码目录结构整理
先从github上下载或者clone一个vue分支项目 https://github.com/vuejs/vue 查看下目录结果 先列出一些目录 Vue |— build 打包相关的配置文件,其中最重 ...
- Golang源码学习:调度逻辑(二)main goroutine的创建
接上一篇继续分析一下runtime.newproc方法. 函数签名 newproc函数的签名为 newproc(siz int32, fn *funcval) siz是传入的参数大小(不是个数):fn ...
- 【JS基础语法】---学习roadmap---6 parts
JS基础语法---roadmap Part 1 - 2: Part 3 - 4: Part 5 - 6
- VUE 源码学习01 源码入口
VUE[version:2.4.1] Vue项目做了不少,最近在学习设计模式与Vue源码,记录一下自己的脚印!共勉!注:此处源码学习方式为先了解其大模块,从宏观再去到微观学习,以免一开始就研究细节然后 ...
- async源码学习 - 全部源码
因为工作需要,可能我离前端走远了,偏node方向了.所以异步编程的需求很多,于是乎,不得不带着学习async了. 我有个习惯,用别人的东西之前,喜欢稍微搞明白点,so就带着看看其源码. github: ...
- Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁
上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列 ...
- Thrift 源码学习一——源码结构
Thrift 客户端与服务端的交互图 源码结构 传输层 TTransport: TTransport:客户端传输层抽象基础类,read.write.flush.close 等方法 TSocket 与 ...
- nginx源码学习_源码结构
nginx的优秀除了体现在程序结构以及代码风格上,nginx的源码组织也同样简洁明了,目录结构层次结构清晰,值得我们去学习.nginx的源码目录与nginx的模块化以及功能的划分是紧密结合,这也使得我 ...
随机推荐
- ASP.NET Core 源码学习之 Options[4]:IOptionsMonitor
前面我们讲到 IOptions 和 IOptionsSnapshot,他们两个最大的区别便是前者注册的是单例模式,后者注册的是 Scope 模式.而 IOptionsMonitor 则要求配置源必须是 ...
- C语言精要总结-指针系列(二)
此文为指针系列第二篇: C语言精要总结-指针系列(一) C语言精要总结-指针系列(二) 指针运算 前面提到过指针的解引用运算,除此之外,指针还能进行部分算数运算.关系运算 指针能进行的有意义的算术运算 ...
- [编织消息框架][netty源码分析]11 UnpooledHeapByteBuf 与 ByteBufAllocator
每种ByteBuf都有相应的分配器ByteBufAllocator,类似工厂模式.我们先学习UnpooledHeapByteBuf与其对应的分配器UnpooledByteBufAllocator 如何 ...
- H5学习第三周
今天主要总结弹性布局 flex使用 1.给父容器添加display flex/inline-flex;属性 2.父容器可以使用的属性值有 >>>flex-direction 属性决定 ...
- 基于邮件系统的远程实时监控系统的实现 Python版
人生苦短,我用Python~ 界内的Python宣传标语,对Python而言,这是种标榜,实际上,Python确实是当下最好用的开发语言之一. 在相继学习了C++/C#/Java之后,接触Python ...
- 普通RAID磁盘数据格式规范
普通RAID磁盘数据格式规范 1.介绍 在当今的IT环境中,系统管理员希望改变他们正在使用的内部RAID方案,原因可能有以下几个:许多服务器都是附带RAID解决方案的,这些RAID解决方案是通过母板磁 ...
- java基础(10) -线程
线程 相当于轻量级进程,线程在程序中是独立的.并发的执行路径,每个线程有它自己的堆栈.自己的程序计数器和自己的局部变量.但是,与分隔的进程相比,进程中的线程之间的隔离程度要小.它们共享内存.文件句柄和 ...
- h5可预览 图片ajax上传 (补更),后台数据获取方法---php
原理是 先获取,然后手动转移文件路径,不然会被服务器自动删除 demo如下: <?php header('content-Type:text/html;charset=utf-8'); $fil ...
- win7-x64安装mysql5.7.11(官方zip版)
1.下载官方安装包(http://www.mysql.com/downloads/),此zip包是没有安装器的(*.msi),也没有辅助配置的自动程序. 2.解压zip包,将文件放入指定目录,如:D: ...
- 谷歌是如何做代码审查的 | 外刊IT评论 - Google Chrome
谷歌是如何做代码审查的 本文的作者 Mark CC 在上一篇文章中提到过,我已经不在Google工作了.我还没有想清楚应该去哪里-有两三个非常好的工作机会摆在我面前.因为在这段做决 ...