Backbone.js源码分析系列之Events模块

来源:互联网 发布:数据来源于 英语 编辑:程序博客网 时间:2024/06/05 19:01

   随着web前端技术的火热,浏览器对js引擎性能的提升,服务端运算逻辑越来越往前端转移,各种各样的前端js框架层出不穷。从之前对对象原型扩展的prototype到后来流行的封装了对DOM操作很便利的,再到现在的对较大型项目代码组织很有利处的各种MV*框架(如Backbone、knockout、angular等),前端的代码正在朝向后端的架构发展演化。

    这些框架的源代码一般水平都较高,代码简洁,逻辑清晰,处理问题往往简单而不简约,阅读框架的源代码不仅对自己增长基本的js语言知识很有好处,学习其组织框架的形式以及提升自己架构的能力也大有利处,同时,如果能对一个框架的源代码有所了解的话对你使用该框架能做到更好的掌控。

        Backbone是一个使用较广泛的MVC框架,其对MVC各部分封装较少,可扩展性比较大,其代码量也较少(加注释一千多行),共分为Events、Model、Collection、View以及Router五大模块。我们将对其源代码按模块进行一一解读,给大家提供一个学习其源代码的机会,同时也在分析其源码的过程中与大家一起进步。由于水平有限,错误在所难免,还望大家不吝指正。

我们分析的Backbone框架版本是1.0.0版本。按照惯例是框架的基本的头部分,定义一些基本的常用方法的变量缓存,以及对根元素window、Backbone对象的定义还有Backbone框架与其他同名框架的共存处理。详见下面注释。

// Initial Setup  // -------------  // 保存对全局对象的引用(浏览器环境中是window,服务器端是server)  var root = this;  // 冲突解决:保存已有的Backbone变量(如果存在),这样的话如果后面使用了`noConflict`以便能够恢复它  var previousBackbone = root.Backbone;  // 创建一个对数组方法的局部引用(后面会用到),提升运行速度  var array = [];  // 从数组尾部加入一个元素,主意返回值是执行完毕后数组的长度值  var push = array.push;  // 获取一个子数组,起另一个重要用途是快速复制一个数组,如:var a = [1,2,3],b = a.slice();则完成了对a的一份复制  var slice = array.slice;  // 可对数组进行删除、添加元素的操作  var splice = array.splice;  // 最高级别的命名空间,Backbone的所有类和模块都将附加在它上面。  var Backbone;  if (typeof exports !== 'undefined') {// 服务器端用exports ???    Backbone = exports;  } else {// 浏览器端用新创建的一个新空对象    Backbone = root.Backbone = {};  }  // 此库的当前版本,与 `package.json`保持同步.  Backbone.VERSION = '1.0.0';  // 让_成为Backbone依赖的库Underscore.js的引用  var _ = root._;  if (!_ && (typeof require !== 'undefined')) _ = require('underscore');  // 为了达成Backbone的功能, 引用jQuery, Zepto, Ender或者自己的拥有`$`全局变量的库  Backbone.$ = root.jQuery || root.Zepto || root.ender || root.$;  // 以*noConflict*模式运行Backbone,将原来的Backbone保存到根对象的Backbone变量上, 并返回一个对新的`Backbone`变量的引用.   Backbone.noConflict = function() {    root.Backbone = previousBackbone;    return this;  };  // 使用`emulateHTTP`来支持遗留的HTTP服务器。设置这个选项将会通过`_method`参数和设置一个`X-Http-Method-Override`的头部来伪造`"PUT"` 和 `"DELETE"`请求。  Backbone.emulateHTTP = false;  // 使用`emulateJSON`来支持那些遗留的不能处理直接的`application/json`请求的服务器。会将请求体(body)编码为`application/x-www-form-urlencoded`,同时将会以一种被命名为`model`的形式参数发送模型(model)  Backbone.emulateJSON = false;

下面开始进入真正的Events模块。Events模块式Backbone的事件模块。它可以被融合到其他任意对象以给那个对象提供常规事件支持。你可以用'on'来绑定一个回调函数到一个事件,可以用'off'解除绑定;`trigger`可以用来触发一个事件从而成功的执行该事件所有的回调。
  //     var object = {};
  //     _.extend(object, Backbone.Events);
  //     object.on('expand', function(){ alert('expanded'); });
  //     object.trigger('expand');
 注意,初学者在这里容易被自定义事件所迷糊,容易跟浏览器里给DOM节点添加事件相混淆。这里所谓的事件其实就是一个事件名(字符串),它对应着很多个回调函数,触发该事件就是让该事件名对应的回调函数执行。jQuery的延迟对象也是类似的道理,只是其中实现机制、所用到的数据结构不一样罢了。

如果以后你自己写框架,要用到类似的事件模块机制,这里的这个模块就可以单独拿出来用。下面是详细注释。

// Backbone.Events  // ---------------  var Events = Backbone.Events = {// 方法说明:绑定一个一个回调函数到一个事件。如果传递的事件名参数是"all",任何事件被触发该回调函数都会被执行。// name:事件名;callback:与事件名相对应的回调函数;context:回调函数执行的上下文环境    on: function(name, callback, context) {  // name参数为空格分隔参数名("event1 event2")以及jQuery式({change:action}),或者callback未传入时返回  // 巧妙地递归思路实现:在name是空格分隔事件名或者jQuery式的对象传入的时候,在eventsApi函数里面会对每一个单字符串事件名一次调用on函数,此时eventsApi会返回true,所以会跳过下面的if继续往下执行,完成绑定。递归是一个非常重要的算法实现思想,它可以让你忽略实现细节,让条理清晰明了。但效率上会相对比较低下,必要时可以考虑转为用非递归的方式实现。      if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;      // name为单个事件名字符串往下执行  this._events || (this._events = {});// this._events[name]不存在则创建一个,并创建其为数组形式 知识点:注意||和&&在表达式中的应用,首先,用"="进行赋值是表达式运算,返回值为最右侧数据;函数执行也是表达式;new一个构造函数也是表达式执行。&&的优先级比||高,||只有在左侧表达式返回false时才执行右侧表达式,而&&正好相反,左侧返回true时才进行右侧表达式执行。||在左侧为true时直接将左侧值返回,否则返回右侧值;&&左侧为false时直接返回左侧值,否则返回右侧值。利用这个特性能写出很多巧妙的代码及运算执行。      var events = this._events[name] || (this._events[name] = []);  // 将传进的参数以此形式push进events[name]数组,说明每一个事件名对应着一系列又先后顺序的对象的数组,其中每一个对象包含了回调函数、执行上下文以及一个较为特殊的ctx属性。围绕着此结构可以执行下面的once、trigger、off以及stopListening等操作。注意此处this的指向,指向拥有on方法的对象实例      events.push({callback: callback, context: context, ctx: context || this});      return this;    },// 给一个事件名绑定只被触发一次的回调函数。在该回调函数第一次被调用后,它江北移除。// name:事件名;callback:回调函数;context:执行上下文环境    once: function(name, callback, context) {// 如果name参数是被空格分隔的事件名格式或者jQuery式的map映射对象格式或者回调函数未传入则返回。给对象添加事件回调其本质还是在触发相应事件名的时候执行回调函数,所以如果回调函数如果没有被传入的话可以不再浪费时间直接不做任何操作,此时将callback初始化为一个空函数也是可以的,但是没有什么意义      if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;      var self = this;// 缓存当前上下文      var once = _.once(function() {// 调用Underscore的once方法,使得被传入的函数只会执行一次。可以去参考Underscore源代码实现,非常巧妙        self.off(name, once);// 解除绑定        callback.apply(this, arguments);// 执行回调      });      once._callback = callback;// 将回调函数附加给once函数的_callback属性以作保存      return this.on(name, once, context);// 执行绑定(注意此时的name已是干净的单字符串形式)    },// 移除一个或多个回调。如果`context`为null,就移除有回调函数参数的事件名的所有回调函数。如果`callback`是null,移除事件多有的回调函数。如果`name`为null,移除绑定到所有事件的所有的回调函数    off: function(name, callback, context) {      var retain, ev, events, names, i, l, j, k;// 在函数开头声明所有的局部变量是一个好的编程习惯  // 递归实现,if之后的所有name都是干净的单字符串事件名      if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;  // name、callback以及context均未传入,将_events对象改变引用,重新指向一个新的空对象(清空_events),并返回this实例对象      if (!name && !callback && !context) {        this._events = {};        return this;      }  // 将names置为数组形式。如果name参数未传入,则去除所有的事件名。  // _.keys(obj)返回一个对象所有属性名组成的字符串      names = name ? [name] : _.keys(this._events);      for (i = 0, l = names.length; i < l; i++) {// 遍历names所有事件名        name = names[i];// 取出一个事件名做缓存,优化性能提升速度        if (events = this._events[name]) { // 取出name事件名对应的所有事件对象数组。该事件名存在的话执行下面的操作          this._events[name] = retain = [];// 先将该事件名对应的事件对象数组置空,后面再添加          if (callback || context) {// callback或者context已传入            for (j = 0, k = events.length; j < k; j++) {              ev = events[j];// 当前事件对象              if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||                  (context && context !== ev.context)) {// 事件对象的callback与传入的callback(若已传入)不同或者与传入的context(若已传入)不同则再将此事件对象push到this._events[name](之前已被置空)                retain.push(ev);              }            }          }          if (!retain.length) delete this._events[name];// 如果name事件名对应的数组为空,干脆将其删除掉        }      }      return this;    },// 触发一个或多个事件,执行所有的绑定回调。回调函数与`trigger`除了函数名外传相同的参数(如果你正在监听"all"事件,将会导致回调接收与第一个参数一样的事件名)。    trigger: function(name) {      if (!this._events) return this;// this._events如果还未创建则无从触发事件,返回      var args = slice.call(arguments, 1);// 取除第一个参数外其他参数,返回值为数组形式      if (!eventsApi(this, 'trigger', name, args)) return this;// 递归执行,if之后的name都是干净的事件名字符串      var events = this._events[name];// 取出name事件名对应的所有事件对象数组      var allEvents = this._events.all;// all事件名对应的所有事件对象组成的数组      if (events) triggerEvents(events, args);// name事件名存在则触发所有的事件      if (allEvents) triggerEvents(allEvents, arguments);// all事件的所有回调在触发任何事件时都会被触发      return this;    },// 告诉一个对象停止监听指定的事件,或者停止监听它正在监听的每一个对象    stopListening: function(obj, name, callback) {      var listeners = this._listeners;// 获得该对象监听的所有对象(以id的形式存储)      if (!listeners) return this;// 如果不存在监听对象则返回,否则继续  // 存在监听对象      var deleteListener = !name && !callback;// 决定删除所有监听      if (typeof name === 'object') callback = this;// 便于传参      if (obj) (listeners = {})[obj._listenerId] = obj;// 指定了被监听对象,则将listeners置为具有属性为obj._listenerId的id、其值为obj的新对象      for (var id in listeners) {// 遍历listeners        listeners[id].off(name, callback, this);// 将obj(如果指定,未指定则是全部被监听对象)解除name事件监听        if (deleteListener) delete this._listeners[id];// 判断监听全删除则全部删除监听的对象      }      return this;    }  };  // 被用来分解事件名字符串的正则(匹配一个或多个空格)  var eventSplitter = /\s+/;  // 根据现有的API来实现事件API的一些比较炫酷的功能特性,比如支持多事件名`"change blur"`以及jQuery式的事件映射`{change: action}`  var eventsApi = function(obj, action, name, rest) {// 如果name参数不存在则不作任何操作    if (!name) return true;    // 处理事件映射    if (typeof name === 'object') {// name参数是对象类型      for (var key in name) {// 遍历name对象的所有可枚举属性// 将obj对象中的action方法以传入name属性名key、key对应的属性值以及参数rest为其参数执行// 一个知识点:一个数组concat另一个数组或元素时返回一个包含两者所有元素的新数组,但原数组保持不变,如:// var a = [1,2],b = a.concat(3); // a:[1,2];b:[1,2,3]        obj[action].apply(obj, [key, name[key]].concat(rest));      }      return false;    }    // 处理以空格分隔的事件名字符串的情况    if (eventSplitter.test(name)) {// 检验事件名字符串是以空格分隔,注意/\s+/.test("change")为false      var names = name.split(eventSplitter);// 将事件名字符串分隔成各事件名字符串组成的数组      for (var i = 0, l = names.length; i < l; i++) {// 遍历事件名数组// 以事件名、参数rest为obj[action]的参数执行该方法。此处不可写为:obj[action].call(obj, names[i],rest);rest代表所有其他参数,方便用数组一次性传入        obj[action].apply(obj, [names[i]].concat(rest));      }      return false;    }// name参数不是:// 1)、以空格分隔多个事件名// 2)、以map映射对象为name参数// 两种情况以及name参数未传入的返回true,否则返回false    return true;  };  // 一个难以置信,但是被优化过了用于触发事件的内部的分发函数。这个函数尝试让一些通常的情形能够快速执行(Backbone大多数的函数事件都有3个参数)  var triggerEvents = function(events, args) {    var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];// Backbone大多数的函数事件都有3个参数,所以这么做能够让一些通常的情形能够快速执行    switch (args.length) {// 根据参数个数不同做不同的操作      case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;      case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;      case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;      case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;      default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args);    }  };  // listenTo、listenToOnce用于监听其他对象的事件  var listenMethods = {listenTo: 'on', listenToOnce: 'once'};  // `on` 和 `once`的反转控制版本。告诉*this*对象来监听另一个对象中的事件,并追踪它所监听的事件  // implementation:'on'/'once'; method:"listenTo"/"listenToOnce"  _.each(listenMethods, function(implementation, method) {// 给Events对象添加"listenTo"/"listenToOnce"方法// obj:监听的对象  name:被监听对象的事件名 callback:回调函数    Events[method] = function(obj, name, callback) {      var listeners = this._listeners || (this._listeners = {});// this对象的_listeners属性,存放被监听的对象。不存在则创建      var id = obj._listenerId || (obj._listenerId = _.uniqueId('l'));// 获得唯一id,不存在则创建      listeners[id] = obj;// 将被监听对象设为id的属性值(obj._listenerId也有该值)      if (typeof name === 'object') callback = this;      obj[implementation](name, callback, this);// 给obj(绑定/绑定只执行一次的)事件,这样的话obj触发相应事件名的时候会执行callback,相当于实现监听      return this;    };  });  //别名,以实现向后兼容  Events.bind   = Events.on;  Events.unbind = Events.off;  // 允许Backbone对象作为一个全局的事件总线,为那些想要在一个方便的地方需要全局"pubsub"的人来服务。  _.extend(Backbone, Events);

注释已经加的很详细了,相信如果你认真去读一遍的话应该会有收获。下一篇文章我会分析Backbone中用于处理数据的Model模块。

0 0