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

最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-study)进行参考交流,有详细的源码注释,以及知识总结,同时 google 一下 backbone 源码,也有很多优秀的文章可以用来学习。

我这里主要记录一些偏设计方向的知识点。具体从以下几个方面入手:
1、MVC 框架
2、观察者模式 以及 控制反转

一、MVC 框架
所谓 MVC 框架,包含三个部分,model 作为模型层、 view 作为视图层、而 controller 则作为控制层。
  * model 模型:用于封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。 model 有对数据直接访问的权力,例如对数据库的访问。“model”不依赖“view”和“controller”,也就是说, model 不关心它会被如何显示或是如何被操作。
  * view 视图:负责显示数据,也就是我们的用户界面。
  * controller 控制器: 起到不同层面间的组织作用,用于控制应用程序的流程。它处理事件并作出响应。“事件”包括用户的行为和数据 model 上的改变。换句话说,它负责根据用户从"view视图层"输入的指令,选取"model数据层"中的数据,然后对其进行相应的操作,产生最终结果。

对于这三个部分是如何通信的,有很多种情况:

传统 mvc:

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

用户通过在 view 上点击或者输入等操作,传指令到 controller,controller 完成业务逻辑后,操作 model 改变数据,model 的变化触发 view 层的改变,显示更改后的数据。三者都是单向联系的。这样的设计,更加适用于视图会长时间存在并且需要频繁根随数据变化的场景,比如传统的客户端程序,web 前端页面。

model2:

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

model2 不同于传统 mvc 的主要区别就是 model 和 view 的完全隔离。直接通过 controller 接受指令,操作访问 model 层,并且 传递数据渲染 view。controller 与 view 和 model 都是单向联系的。这样的设计更加适用于 web 服务后端,控制器接受到的事件来源很统一,绝大部份是网络请求。而每个网络请求的结果多是产生一个 view 的 render,每一个 view 之间都是独立而短暂的,任何需要反映出 model 的变化,都需要产生一个新的 view。

再来说说 backbone 中的 mvc:
backbone 中有如下几个模块:

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

这里你会有个问题,backbone 中并没有定义 controller,那么还是真正的 mvc 吗?

先来说别的模块:

集合 collection ,它是一组 model 的集合,通过 collection 可以将一组数据结构相同的 model 有序地组织在一起,进行批量操作和管理等。
视图 view 是基于 Backbone.js 开发的 Web App 中的核心部分,负责用户交互事件的捕捉和处理、把用户输入导向 model 或 collection、渲染视图、操作DOM等。

可以看出来:
Backbone.js 中的 model 和 collection 共同构成了 MVC 中的 model 层。
Backbone.js 中的 view 既是 MVC 中的 view 层,同时也承担了 controller 的职责。这样就导致 view 非常厚,业务逻辑都部署在了 view 层。

在起初的 backbone 代码中,router 组件的名称是 controller,这很容易直接联系到 MVC 中的 C ,但事实上,backbone 中的 controller 仅仅是根据 URL hash 来在对应的行为和实践中做路由的,与真正意义上的 C 相比简单的多,因此在0.5版本前后 controller 改名为 router 了。

这些模块又是如何通信的?
model 和 view 和 sync
backbone 中 model ,可以被添加、验证、销毁或者是保存到服务器。当你进行交互操作,比如用户输入,引起一个 数据 model 中的属性变化时,model 会调用 sync 模块,用于保存数据到数据库,同时触发一个“change”事件,通知所有的和这个 model 有关的视图层也就是 view 层数据有改变,然后 view 会做出相应地反应,重新呈现新数据。

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

collection 和 view 和 sync
collection 是 一组 model 的集合,帮助你批量管理相关的 model。当你进行交互操作,比如用户输入,添加一个新的 model ,这个时候会在 collection 的创建一个 model ,然后调用 sync 模块,保存新 model 到数据库,同时触发一个“add”事件,通知所有的和这个 model 有关的视图层也就是 view 层数据有改变,然后 view 会做出相应地反应,重新呈现新数据。

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

那从技术角度是如何实现 model 变换通知 view 的呢?这里就要提到观察者模式以及控制反转了。

二、观察者模式以及控制反转
观察者模式:即订阅/发布模式,一种设计模式。它是由两类对象组成,主题和观察者,主题负责发布事件,同时观察者通过订阅这些事件来观察该主体,发布者和订阅者是完全解耦的,彼此不知道对方的存在,两者仅仅共享一个自定义事件的名称。发布者自动将自身的状态的任何变化通知给观察者。在mvc框架中,核心是m(模型)->v(视图)->c(控制器)的交互通信过程,观察者模式是驱动它们的核心模式之一。

举个生活中的例子方便理解:
对于报纸的订阅投送,首先是读者,它们是订阅者,可以选择自己的居住地点,让报纸送到自己的家中。另一个角色是发行方,它们负责出版报纸。作为订阅者,数据到来的时候我们收到通知,我们消费数据,然后根据数据作出反应。只要报纸到了订阅者手中,它们就可以自行处置,有些人读完之后会将其扔到一边,有些人会向朋友转述看到的新闻,甚至还有一些会把报纸送回去。总而言之,订阅者要从发行方接收数据。作为发行方,则要发送数据。一般说来,一个发行方可能有许多订阅者,同样一个订阅者也可能会订阅多家报社的报纸。这是一种多对多的关系,需要一种策略使得订阅者能够彼此独立的发生改变,发行方能够接受任何有消费意识的订阅者。

再举个例子:
去公司面试,结束的时候,面试官对我说:“请留下你的联系方式,有消息我们会通知你”。在这里 我 就是一个订阅者,面试官是发布者,我不需要每天打电话询问面试结果,通讯的主动权掌握在面试官手上,我只需要告诉他我的联系方式。

在很多资料上面,人们认为 订阅/发布模式 和 观察者模式 是有不同的。具体的区别体现在以下两方面:
1、观察者模式主要以同步方式实现,即当某些事件发生时,被观察者可以调用所有观察者的适当方法。 而发布/订阅模式主要以异步方式实现(使用消息队列)。
2、观察者模式中,观察者 知道 被观察者。 而在发布/订阅模式中,发布者 和 订阅者 不需要彼此了解。 他们只是在消息队列的帮助下进行沟通。

在我看来,你现在其实不需要去过分纠结它们的区别,重要的是要理解他们的思想。

回到 backbone 中, 我们看看 backbone 中如何利用观察者模式。

在backbone中, events 自定义事件 模块是核心模块之一。 它在 backbone 的开头最先定义,之后所有的模块都通过

 _.extend(某个模块.prototype, Events, {
//...........这里定义了 某个模块 自己的一些方法...........
})

继承了Events,这样所有的模块,像 Backbone.Model,Backbone.Collection,Backbone.View,Backbone.Router 等都可以使用 Events 的属性。
 // 绑定事件。将一个事件绑定到 `callback` 函数上。事件触发时执行回调函数`callback`。
Events.on = function(name, callback, context) {}; // “on”的控制反转版本。
Events.listenTo = function(obj, name, callback) {}; // 此函数作用于删除一个或多个回调。
Events.off = function(name, callback, context) {}; // 解除 当前 object 监听的 其他对象上制定事件,或者说是所有当前监听的事件。
Events.stopListening = function(obj, name, callback) {}; // 绑定事件只能触发一次。在第一次调用回调之后,它的监听器将被删除。如果使用空格分隔的语法传递多个事件,则处理程序将针对每个事件触发一次,而不是一次所有事件的组合。
Events.once = function(name, callback, context) {}; // once的反转控制版本
Events.listenToOnce = function(obj, name, callback) {}; // 触发一个或者多个事件,并触发所有的回调函数
Events.trigger = function(name) {}; // 实例,保存当前对象所监听的对象
var Listening = function(listener, obj) {
this.id = listener._listenId; //监听方的id
this.listener = listener; // 监听方
this.obj = obj; // 被监听的对象
this.interop = true;
this.count = 0; //监听了几个事件
this._events = void 0; // 监听事件的回调函数序列
}; // Listening的实例可以有 on 方法绑定事件
Listening.prototype.on = Events.on; // Listening的实例用来解除正在监听的一个或多个回调。
Listening.prototype.off = function(name, callback) {}; // 清理监听方和事件列表之间的内存绑定。
Listening.prototype.cleanup = function() {}; // 等价函数命名
Events.bind = Events.on;
Events.unbind = Events.off;

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

这里先简单提一个概念--控制反转,上面的 Events.listenTo 就是 Events.on 的控制反转实现形式,Events.listenToOnce 就是 Events.once 的控制反转实现形式。

控制反转(Inversion of Control,缩写为IoC),这是一种主从关系的转变,一种是 A 直接控制 B ,另一种用控制器(listenTo方法)间接的让 A 控制 B 。
举个例子:
B 对象上面发生 b 事件的时候,通知 A 调用回调函数。

A.listenTo(B, “b”, callback);

当然也可以用 on 来实现同样的功能

B.on(“b”, callback, A);

控制反转 的思想其实应用在了很多地方,这里不详细讲了,后面会有专门一篇文章说一下控制反转。这里你只要知道 调用了 Event.listenTo 方法,会使得B 对象上面发生 b 事件的时候,通知 A 调用回调函数。那么应用在 mvc 之间的通信中,view.listenTo(model,”change”,changeView); 就可以实现当 model 发生变化的时候通知相应的 View 发生改变。

回到观察者模式,咱们从头梳理一下,它是如何实现的。
首先看绑定事件:
所有的绑定事件,无论是 listenTo 还是 once,最后都会通过调用 Events.on 方法进行绑定,而在 on 方法中

 // 绑定事件。将一个事件绑定到 `callback` 函数上。事件触发时执行回调函数`callback`。
// 典型调用方式是`object.on('name', callback, context)`.
// `name`是监听的事件名, `callback`是事件触发时的回调函数, `context`是回调函数上下文,即回调函数中的This(未指定时就默认为当前`object`).
// 如果传递参数 `"all",任何事件的发生都会触发该回调函数。回调函数的第一个参数会传递该事件的名称。举个例子,将一个对象的所有事件代理到另一对象:
// 例子:
// proxy.on("all", function(eventName) {
// object.trigger(eventName);
// });
Events.on = function(name, callback, context) {
// this._events 保存所有监听事件
// 调用 onApi 用来绑定事件
// eventsApi函数参数(iteratee, events, name, callback, opts)
// 参数中 如果还没有this._events,那么就初始化为空对象。
//
// opts中参数:
// callback 事件的回调函数
// context 回调函数的上下文对象(即当调用on时,为context参数,当调用view.listenTo(....)时,为调用的对象如:view。)
// ctx 为context ,当context不存在时,为被监听的对象,如:model.on(…)或view.on(model,…)中的model
// listening 其实就是view._listeningTo中的某个属性值,可以看成: listening == view._listeningTo[‘l1’]
this._events = eventsApi(onApi, this._events || {},
name, callback, {
context: context,
ctx: this,
listening: _listening
}); // 处理通过 listenTo 方法调用 on 绑定的情况
// 在下方定义的 Events.listenTo 中会调用 on 方法来绑定事件,当你调用listenTo方法的时候(如下一行的例子1)这个时候就会产生有 _listening 的情况。
// 例子1:A.listenTo(B, “b”, callback);
// _listening:在下方 Events.listenTo 方法中,被赋值为正在监听的对象的id,例子1中的 B 的 id。赋值语句如下:
// var listening = _listening = listeningTo[id];
// 结合下方的 listenTo 方法来理解这个变量
if (_listening) {
// 定义变量监听者 listener,赋值 this._listeners;如果还没有this._listeners,初始化为空对象。
var listeners = this._listeners || (this._listeners = {});
// 将上文定义的私有全局变量_listening 赋值给 listeners[_listening.id]; 即 监听者监听的对象id。
listeners[_listening.id] = _listening;
// Allow the listening to use a counter, instead of tracking
// callbacks for library interop
// todo
// 允许 listening 使用计数器,而不是跟踪库互操作性回调
_listening.interop = false;
} // 返回 this
return this;
};

20~25行:this._events 用于将订阅者缓存到对象中,

而在触发事件 Events.trigger 方法中

 // 触发一个或者多个事件,并触发所有的回调函数
Events.trigger = function(name) {
// 每个Events对象内部有一个_events对象,保存某一个事件的回调函数队列。
// 如果没有监听事件,则直接返回
if (!this._events) return this; // 参数长度
var length = Math.max(0, arguments.length - 1);
// 新建一个数组
var args = Array(length);
// 在数组args中保存传递进来的除了第一个之外的其余参数,提取出来的参数最终回传递给下方定义的函数 triggerApi
for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; // 调用下方定义的triggerApi
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};

其中 15 行:通过 eventsApi(triggerApi, this._events, name, void 0, args); 用于发布之前的缓存方法。

有了这两个方法 就可以实现 订阅 与 发布模式了,那么该模式到底在 backbone 中的哪里体现了呢?

Events 模块应用到的地方非常之多,在上面我们就已经说过,backbone 的所有模块都通过 extend 方法继承了 Events 中所有方法。在backbone中,我们需要自行实现数据(model)和视图(view)绑定,也就是说在 view 初始化的时候,我们需要绑定对应 model 的关系,下面是一个 view 和 model 绑定的例子:

 var Todo = Backbone.Model.extend({
model.trigger('destroy');
}); var TodoView = Backbone.View.extend({
events: {
"click a.destroy" : "clear",
}, initialize: function() {
this.listenTo(this.model, 'destroy', this.remove);
}, clear: function() {
this.model.destroy();
}, remove: function() {
this.$el.remove();
} });

这段代码不难看懂。页面中有个 a 标签,当你点击之后 会执行 clear 方法,使得当前绑定的 model 执行 destroy 方法,而这就会触发 当前 view 的 $el 被删除,这是因为 initialize 方法中

this.listenTo(this.model, 'destroy', this.remove);

这里就用到了控制反转。当前 view 监听了 当前 model 的 destroy 方法,如果 model 的destroy 被触发,view 会调用 自身的 remove 方法。

此处 view 就相当于 订阅者,他订阅了 model 的 destroy 方法的调用,而 model 就相当于 发布者,他 trigger 了 destroy 方法,通知了 view 调用了 this.remove 方法。

最后:具体的 backbone 代码关于这部分的实现,还是推荐大家自己研究一遍源码,可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-study)进行参考交流,有详细的源码注释,以及知识总结。

学习并感谢: 
http://neekey.net/2016/05/07/%E7%90%86%E8%A7%A3-mvc-model2-mvp-mvvm-flux/   (推荐想要了解 理解 MVC / Model2 / MVP / MVVM / Flux 之间区别的同学阅读一下)
上一篇:机器学习:Python中如何使用支持向量机(SVM)算法


下一篇:【原】AFNetworking源码阅读(三)