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

时间:2021-02-14 05:36:35

最近看完了 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 个对象组成的,这些对象通过彼此的合作,最终实现业务逻辑。就像下面这个图:

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

类似于机械手表,齿轮之间互相带动,互相影响,在这种方式的协同工作中,若一个齿轮出现问题不转了,那么其他齿轮也会受到影响停止转动。
对象之间的耦合关系是无法避免的,因为他们要互相配合才能完成工作,当程序功能越来越庞大,对象之间的依赖关系也就越复杂,会出现对象之间的多重依赖关系,就像下面这个图,关系是错综复杂的:

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

这个时候如果一个对象的改变,需要和其相关的所有对象都作出改变,牵一发而动全身,一是关系不好理清,二是工作量加大,三是模块的可复用性低。

为了解决这一问题,降低对象模块之间的低耦合,控制反转(IoC)理论诞生了。
这个理论希望我们把复杂的功能需求,业务逻辑,拆分成相互合作的对象,这些对象通过封装以后,可以更加灵活地被重用和扩展,然后借助“第三方”实现具有依赖关系,但是又是低耦合的合作方式:

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

通过“第三方”,即 IoC 容器,对象之间的耦合明显降低,各个齿轮的转动都是依靠 “第三方”,所有对象的控制权也都是 “第三方” IoC 容器 来管理。正是 IoC 容器把所有对象粘合在一起发挥作用,如果没有它,对象与对象之间彼此会失去联系。

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

咱们来比较一下 有无引入 IoC 容器 的区别:
  A、对于没有引入 IoC 容器的设计来说,就像第一张图

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

Object A 依赖于 Object B,当 Object A 在初始化或者运行到某一点需要 Object B 支持的时候,Object A 必须主动去创建 Object B 或者使用已经创建的 Object B。无论是创建还是使用已经创建了的 Object B,控制权都在 Object  A 自己手上。

  B、而对于引入 IoC 容器的设计来说,就像第三张图

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

由于 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,但其实他只是用到了很小的方面,只是思想的符合,而真正意义上的控制反转则大面积的运用到了依赖管理中,通过这篇文章,你应该可以有个系统的认识了。

学习并感谢:

https://my.oschina.net/1pei/blog/492601   控制反转IOC与依赖注入DI

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