zoukankan      html  css  js  c++  java
  • 【 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:

    用户通过在 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 })

    继承了Events,这样所有的模块,像 Backbone.Model,Backbone.Collection,Backbone.View,Backbone.Router 等都可以使用 Events 的属性。
     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)进行参考交流,有详细的源码注释,以及知识总结。

    学习并感谢: 
    http://neekey.net/2016/05/07/%E7%90%86%E8%A7%A3-mvc-model2-mvp-mvvm-flux/   (推荐想要了解 理解 MVC / Model2 / MVP / MVVM / Flux 之间区别的同学阅读一下)
  • 相关阅读:
    读书笔记-《编写可读代码的艺术》一
    maven报错Error:(4, 35) java:程序包org.springframework.context不存在
    AutoCAD.Net/C#.Net QQ群:193522571 当需要把wipeout加入到block中时,必须把wipeout放在objectidcollection中的第一位
    AutoCAD.Net/C#.Net QQ群:193522571 绘制椭圆及椭圆弧
    AutoCAD.Net/C#.Net QQ群:193522571 Nested Select后,如果有上一级图元则ResultNestedContainer不为Null,从小到大,从父亲到爷爷
    AutoCAD.Net/C#.Net QQ群:193522571 标注对象Dimension中的DimensionText和Measurement的区别
    AutoCAD.Net/C#.Net QQ群:193522571 同一套窗体代码,同时用在Winform、PvBox和PvTools中
    AutoCAD.Net/C#.Net QQ群:193522571 当用户使用的不是默认的WCS坐标系时,打印程序容易打成空白,因为点没有转换
    AutoCAD.Net/C#.Net QQ群:193522571 ComBobox绑定Datatable并去除重复!
    AutoCAD.Net/C#.Net QQ群:193522571 字段包含于一个字符串的SQL
  • 原文地址:https://www.cnblogs.com/lijiayi/p/backbone1.html
Copyright © 2011-2022 走看看