最近看完了 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:
用户通过在 view 上点击或者输入等操作,传指令到 controller,controller 完成业务逻辑后,操作 model 改变数据,model 的变化触发 view 层的改变,显示更改后的数据。三者都是单向联系的。这样的设计,更加适用于视图会长时间存在并且需要频繁根随数据变化的场景,比如传统的客户端程序,web 前端页面。
model2:
model2 不同于传统 mvc 的主要区别就是 model 和 view 的完全隔离。直接通过 controller 接受指令,操作访问 model 层,并且 传递数据渲染 view。controller 与 view 和 model 都是单向联系的。这样的设计更加适用于 web 服务后端,控制器接受到的事件来源很统一,绝大部份是网络请求。而每个网络请求的结果多是产生一个 view 的 render,每一个 view 之间都是独立而短暂的,任何需要反映出 model 的变化,都需要产生一个新的 view。
再来说说 backbone 中的 mvc:
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 会做出相应地反应,重新呈现新数据。
collection 和 view 和 sync
collection 是 一组 model 的集合,帮助你批量管理相关的 model。当你进行交互操作,比如用户输入,添加一个新的 model ,这个时候会在 collection 的创建一个 model ,然后调用 sync 模块,保存新 model 到数据库,同时触发一个“add”事件,通知所有的和这个 model 有关的视图层也就是 view 层数据有改变,然后 view 会做出相应地反应,重新呈现新数据。
那从技术角度是如何实现 model 变换通知 view 的呢?这里就要提到观察者模式以及控制反转了。
二、观察者模式以及控制反转
观察者模式:即订阅/发布模式,一种设计模式。它是由两类对象组成,主题和观察者,主题负责发布事件,同时观察者通过订阅这些事件来观察该主体,发布者和订阅者是完全解耦的,彼此不知道对方的存在,两者仅仅共享一个自定义事件的名称。发布者自动将自身的状态的任何变化通知给观察者。在mvc框架中,核心是m(模型)->v(视图)->c(控制器)的交互通信过程,观察者模式是驱动它们的核心模式之一。
举个生活中的例子方便理解:
对于报纸的订阅投送,首先是读者,它们是订阅者,可以选择自己的居住地点,让报纸送到自己的家中。另一个角色是发行方,它们负责出版报纸。作为订阅者,数据到来的时候我们收到通知,我们消费数据,然后根据数据作出反应。只要报纸到了订阅者手中,它们就可以自行处置,有些人读完之后会将其扔到一边,有些人会向朋友转述看到的新闻,甚至还有一些会把报纸送回去。总而言之,订阅者要从发行方接收数据。作为发行方,则要发送数据。一般说来,一个发行方可能有许多订阅者,同样一个订阅者也可能会订阅多家报社的报纸。这是一种多对多的关系,需要一种策略使得订阅者能够彼此独立的发生改变,发行方能够接受任何有消费意识的订阅者。
再举个例子:
去公司面试,结束的时候,面试官对我说:“请留下你的联系方式,有消息我们会通知你”。在这里 我 就是一个订阅者,面试官是发布者,我不需要每天打电话询问面试结果,通讯的主动权掌握在面试官手上,我只需要告诉他我的联系方式。
在很多资料上面,人们认为 订阅/发布模式 和 观察者模式 是有不同的。具体的区别体现在以下两方面:
1、观察者模式主要以同步方式实现,即当某些事件发生时,被观察者可以调用所有观察者的适当方法。 而发布/订阅模式主要以异步方式实现(使用消息队列)。
2、观察者模式中,观察者 知道 被观察者。 而在发布/订阅模式中,发布者 和 订阅者 不需要彼此了解。 他们只是在消息队列的帮助下进行沟通。
在我看来,你现在其实不需要去过分纠结它们的区别,重要的是要理解他们的思想。
回到 backbone 中, 我们看看 backbone 中如何利用观察者模式。
在backbone中, events 自定义事件 模块是核心模块之一。 它在 backbone 的开头最先定义,之后所有的模块都通过
1 _.extend(某个模块.prototype, Events, { 2 //...........这里定义了 某个模块 自己的一些方法........... 3 })
1 // 绑定事件。将一个事件绑定到 `callback` 函数上。事件触发时执行回调函数`callback`。 2 Events.on = function(name, callback, context) {}; 3 4 // “on”的控制反转版本。 5 Events.listenTo = function(obj, name, callback) {}; 6 7 // 此函数作用于删除一个或多个回调。 8 Events.off = function(name, callback, context) {}; 9 10 // 解除 当前 object 监听的 其他对象上制定事件,或者说是所有当前监听的事件。 11 Events.stopListening = function(obj, name, callback) {}; 12 13 // 绑定事件只能触发一次。在第一次调用回调之后,它的监听器将被删除。如果使用空格分隔的语法传递多个事件,则处理程序将针对每个事件触发一次,而不是一次所有事件的组合。 14 Events.once = function(name, callback, context) {}; 15 16 // once的反转控制版本 17 Events.listenToOnce = function(obj, name, callback) {}; 18 19 // 触发一个或者多个事件,并触发所有的回调函数 20 Events.trigger = function(name) {}; 21 22 // 实例,保存当前对象所监听的对象 23 var Listening = function(listener, obj) { 24 this.id = listener._listenId; //监听方的id 25 this.listener = listener; // 监听方 26 this.obj = obj; // 被监听的对象 27 this.interop = true; 28 this.count = 0; //监听了几个事件 29 this._events = void 0; // 监听事件的回调函数序列 30 }; 31 32 // Listening的实例可以有 on 方法绑定事件 33 Listening.prototype.on = Events.on; 34 35 // Listening的实例用来解除正在监听的一个或多个回调。 36 Listening.prototype.off = function(name, callback) {}; 37 38 // 清理监听方和事件列表之间的内存绑定。 39 Listening.prototype.cleanup = function() {}; 40 41 // 等价函数命名 42 Events.bind = Events.on; 43 Events.unbind = Events.off;
这里先简单提一个概念--控制反转,上面的 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 方法中
1 // 绑定事件。将一个事件绑定到 `callback` 函数上。事件触发时执行回调函数`callback`。 2 // 典型调用方式是`object.on('name', callback, context)`. 3 // `name`是监听的事件名, `callback`是事件触发时的回调函数, `context`是回调函数上下文,即回调函数中的This(未指定时就默认为当前`object`). 4 // 如果传递参数 `"all",任何事件的发生都会触发该回调函数。回调函数的第一个参数会传递该事件的名称。举个例子,将一个对象的所有事件代理到另一对象: 5 // 例子: 6 // proxy.on("all", function(eventName) { 7 // object.trigger(eventName); 8 // }); 9 Events.on = function(name, callback, context) { 10 // this._events 保存所有监听事件 11 // 调用 onApi 用来绑定事件 12 // eventsApi函数参数(iteratee, events, name, callback, opts) 13 // 参数中 如果还没有this._events,那么就初始化为空对象。 14 // 15 // opts中参数: 16 // callback 事件的回调函数 17 // context 回调函数的上下文对象(即当调用on时,为context参数,当调用view.listenTo(....)时,为调用的对象如:view。) 18 // ctx 为context ,当context不存在时,为被监听的对象,如:model.on(…)或view.on(model,…)中的model 19 // listening 其实就是view._listeningTo中的某个属性值,可以看成: listening == view._listeningTo[‘l1’] 20 this._events = eventsApi(onApi, this._events || {}, 21 name, callback, { 22 context: context, 23 ctx: this, 24 listening: _listening 25 }); 26 27 // 处理通过 listenTo 方法调用 on 绑定的情况 28 // 在下方定义的 Events.listenTo 中会调用 on 方法来绑定事件,当你调用listenTo方法的时候(如下一行的例子1)这个时候就会产生有 _listening 的情况。 29 // 例子1:A.listenTo(B, “b”, callback); 30 // _listening:在下方 Events.listenTo 方法中,被赋值为正在监听的对象的id,例子1中的 B 的 id。赋值语句如下: 31 // var listening = _listening = listeningTo[id]; 32 // 结合下方的 listenTo 方法来理解这个变量 33 if (_listening) { 34 // 定义变量监听者 listener,赋值 this._listeners;如果还没有this._listeners,初始化为空对象。 35 var listeners = this._listeners || (this._listeners = {}); 36 // 将上文定义的私有全局变量_listening 赋值给 listeners[_listening.id]; 即 监听者监听的对象id。 37 listeners[_listening.id] = _listening; 38 // Allow the listening to use a counter, instead of tracking 39 // callbacks for library interop 40 // todo 41 // 允许 listening 使用计数器,而不是跟踪库互操作性回调 42 _listening.interop = false; 43 } 44 45 // 返回 this 46 return this; 47 };
20~25行:this._events 用于将订阅者缓存到对象中,
而在触发事件 Events.trigger 方法中
1 // 触发一个或者多个事件,并触发所有的回调函数 2 Events.trigger = function(name) { 3 // 每个Events对象内部有一个_events对象,保存某一个事件的回调函数队列。 4 // 如果没有监听事件,则直接返回 5 if (!this._events) return this; 6 7 // 参数长度 8 var length = Math.max(0, arguments.length - 1); 9 // 新建一个数组 10 var args = Array(length); 11 // 在数组args中保存传递进来的除了第一个之外的其余参数,提取出来的参数最终回传递给下方定义的函数 triggerApi 12 for (var i = 0; i < length; i++) args[i] = arguments[i + 1]; 13 14 // 调用下方定义的triggerApi 15 eventsApi(triggerApi, this._events, name, void 0, args); 16 return this; 17 };
其中 15 行:通过 eventsApi(triggerApi, this._events, name, void 0, args); 用于发布之前的缓存方法。
有了这两个方法 就可以实现 订阅 与 发布模式了,那么该模式到底在 backbone 中的哪里体现了呢?
Events 模块应用到的地方非常之多,在上面我们就已经说过,backbone 的所有模块都通过 extend 方法继承了 Events 中所有方法。在backbone中,我们需要自行实现数据(model)和视图(view)绑定,也就是说在 view 初始化的时候,我们需要绑定对应 model 的关系,下面是一个 view 和 model 绑定的例子:
1 var Todo = Backbone.Model.extend({ 2 model.trigger('destroy'); 3 }); 4 5 var TodoView = Backbone.View.extend({ 6 events: { 7 "click a.destroy" : "clear", 8 }, 9 10 initialize: function() { 11 this.listenTo(this.model, 'destroy', this.remove); 12 }, 13 14 clear: function() { 15 this.model.destroy(); 16 }, 17 18 remove: function() { 19 this.$el.remove(); 20 } 21 22 });
这段代码不难看懂。页面中有个 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)进行参考交流,有详细的源码注释,以及知识总结。