幽灵资源网 Design By www.bzswh.com
// Backbone.js 0.9.2

 

// (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc.

// Backbone may be freely distributed under the MIT license.

// For all details and documentation:

// http://backbonejs.org

(function() {

 

  // 创建一个全局对象, 在浏览器中表示为window对象, 在Node.js中表示global对象

  var root = this;

 

  // 保存"Backbone"变量被覆盖之前的值

  // 如果出现命名冲突或考虑到规范, 可通过Backbone.noConflict()方法恢复该变量被Backbone占用之前的值, 并返回Backbone对象以便重新命名

  var previousBackbone = root.Backbone;

 

  // 将Array.prototype中的slice和splice方法缓存到局部变量以供调用

  var slice = Array.prototype.slice;

  var splice = Array.prototype.splice;

 

  var Backbone;

  if( typeof exports !== 'undefined') {

    Backbone = exports;

  } else {

    Backbone = root.Backbone = {};

  }

 

  // 定义Backbone版本

  Backbone.VERSION = '0.9.2';

 

  // 在服务器环境下自动导入Underscore, 在Backbone中部分方法依赖或继承自Underscore

  var _ = root._;

  if(!_ && ( typeof require !== 'undefined'))

    _ = require('underscore');

 

  // 定义第三方库为统一的变量"$", 用于在视图(View), 事件处理和与服务器数据同步(sync)时调用库中的方法

  // 支持的库包括jQuery, Zepto等, 它们语法相同, 但Zepto更适用移动开发, 它主要针对Webkit内核浏览器

  // 也可以通过自定义一个与jQuery语法相似的自定义库, 供Backbone使用(有时我们可能需要一个比jQuery, Zepto更轻巧的自定义版本)

  // 这里定义的"$"是局部变量, 因此不会影响在Backbone框架之外第三方库的正常使用

  var $ = root.jQuery || root.Zepto || root.ender;

 

  // 手动设置第三方库

  // 如果在导入了Backbone之前并没有导入第三方库, 可以通过setDomLibrary方法设置"$"局部变量

  // setDomLibrary方法也常用于在Backbone中动态导入自定义库

  Backbone.setDomLibrary = function(lib) {

    $ = lib;

  };

  // 放弃以"Backbone"命名框架, 并返回Backbone对象, 一般用于避免命名冲突或规范命名方式

  // 例如:

  // var bk = Backbone.noConflict(); // 取消"Backbone"命名, 并将Backbone对象存放于bk变量中

  // console.log(Backbone); // 该变量已经无法再访问Backbone对象, 而恢复为Backbone定义前的值

  // var MyBackbone = bk; // 而bk存储了Backbone对象, 我们将它重命名为MyBackbone

  Backbone.noConflict = function() {

    root.Backbone = previousBackbone;

    return this;

  };

  // 对于不支持REST方式的浏览器, 可以设置Backbone.emulateHTTP = true

  // 与服务器请求将以POST方式发送, 并在数据中加入_method参数标识操作名称, 同时也将发送X-HTTP-Method-Override头信息

  Backbone.emulateHTTP = false;

 

  // 对于不支持application/json编码的浏览器, 可以设置Backbone.emulateJSON = true;

  // 将请求类型设置为application/x-www-form-urlencoded, 并将数据放置在model参数中实现兼容

  Backbone.emulateJSON = false;

 

  // Backbone.Events 自定义事件相关

  // -----------------

 

  // eventSplitter指定处理多个事件时, 事件名称的解析规则

  var eventSplitter = /\s+/;

 

  // 自定义事件管理器

  // 通过在对象中绑定Events相关方法, 允许向对象添加, 删除和触发自定义事件

  var Events = Backbone.Events = {

 

    // 将自定义事件(events)和回调函数(callback)绑定到当前对象

    // 回调函数中的上下文对象为指定的context, 如果没有设置context则上下文对象默认为当前绑定事件的对象

    // 该方法类似与DOM Level2中的addEventListener方法

    // events允许指定多个事件名称, 通过空白字符进行分隔(如空格, 制表符等)

    // 当事件名称为"all"时, 在调用trigger方法触发任何事件时, 均会调用"all"事件中绑定的所有回调函数

    on : function(events, callback, context) {

      // 定义一些函数中使用到的局部变量

      var calls, event, node, tail, list;

      // 必须设置callback回调函数

      if(!callback)

        return this;

      // 通过eventSplitter对事件名称进行解析, 使用split将多个事件名拆分为一个数组

      // 一般使用空白字符指定多个事件名称

      events = events.split(eventSplitter);

      // calls记录了当前对象中已绑定的事件与回调函数列表

      calls = this._callbacks || (this._callbacks = {});

 

      // 循环事件名列表, 从头至尾依次将事件名存放至event变量

      while( event = events.shift()) {

        // 获取已经绑定event事件的回调函数

        // list存储单个事件名中绑定的callback回调函数列表

        // 函数列表并没有通过数组方式存储, 而是通过多个对象的next属性进行依次关联

        /** 数据格式如:

         * {

         *   tail: {Object},

         *   next: {

         *     callback: {Function},

         *     context: {Object},

         *     next: {

         *       callback: {Function},

         *       context: {Object},

         *       next: {Object}

         *     }

         *   }

         * }

         */

        // 列表每一层next对象存储了一次回调事件相关信息(函数体, 上下文和下一次回调事件)

        // 事件列表最顶层存储了一个tail对象, 它存储了最后一次绑定回调事件的标识(与最后一次回调事件的next指向同一个对象)

        // 通过tail标识, 可以在遍历回调列表时得知已经到达最后一个回调函数

        list = calls[event];

        // node变量用于记录本次回调函数的相关信息

        // tail只存储最后一次绑定回调函数的标识

        // 因此如果之前已经绑定过回调函数, 则将之前的tail指定给node作为一个对象使用, 然后创建一个新的对象标识给tail

        // 这里之所以要将本次回调事件添加到上一次回调的tail对象, 是为了让回调函数列表的对象层次关系按照绑定顺序排列(最新绑定的事件将被放到最底层)

        node = list "all"事件列表

      all = calls.all;

      // 将需要触发的事件名称, 按照eventSplitter规则解析为一个数组

      events = events.split(eventSplitter);

      // 将trigger从第2个之后的参数, 记录到rest变量, 将依次传递给回调函数

      rest = slice.call(arguments, 1);

 

      // 循环需要触发的事件列表

      while( event = events.shift()) {

        // 此处的node变量记录了当前事件的所有回调函数列表

        if( node = calls[event]) {

          // tail变量记录最后一次绑定事件的对象标识

          tail = node.tail;

          // node变量的值, 按照事件的绑定顺序, 被依次赋值为绑定的单个回调事件对象

          // 最后一次绑定的事件next属性, 与tail引用同一个对象, 以此作为是否到达列表末尾的判断依据

          while(( node = node.next) !== tail) {

            // 执行所有绑定的事件, 并将调用trigger时的参数传递给回调函数

            node.callback.apply(node.context || this, rest);

          }

        }

        // 变量all记录了绑定时的"all"事件, 即在调用任何事件时, "all"事件中的回调函数均会被执行

        // - "all"事件中的回调函数无论绑定顺序如何, 都会在当前事件的回调函数列表全部执行完毕后再依次执行

        // - "all"事件应该在触发普通事件时被自动调用, 如果强制触发"all"事件, 事件中的回调函数将被执行两次

        if( node = all) {

          tail = node.tail;

          // 与调用普通事件的回调函数不同之处在于, all事件会将当前调用的事件名作为第一个参数传递给回调函数

          args = [event].concat(rest);

          // 遍历并执行"all"事件中的回调函数列表

          while(( node = node.next) !== tail) {

            node.callback.apply(node.context || this, args);

          }

        }

      }

 

      return this;

    }

  };

 

  // 绑定事件与释放事件的别名, 也为了同时兼容Backbone以前的版本

  Events.bind = Events.on;

  Events.unbind = Events.off;

 

  // Backbone.Model 数据对象模型

  // --------------

 

  // Model是Backbone中所有数据对象模型的基类, 用于创建一个数据模型

  // @param {Object} attributes 指定创建模型时的初始化数据

  // @param {Object} options

  /**

   * @format options

   * {

   *   parse: {Boolean},

   *   collection: {Collection}

   * }

   */

  var Model = Backbone.Model = function(attributes, options) {

    // defaults变量用于存储模型的默认数据

    var defaults;

    // 如果没有指定attributes参数, 则设置attributes为空对象

    attributes || ( attributes = {});

    // 设置attributes默认数据的解析方法, 例如默认数据是从服务器获取(或原始数据是XML格式), 为了兼容set方法所需的数据格式, 可使用parse方法进行解析

    if(options && options.parse)

      attributes = this.parse(attributes);

    if( defaults = getValue(this, 'defaults')) {

      // 如果Model在定义时设置了defaults默认数据, 则初始化数据使用defaults与attributes参数合并后的数据(attributes中的数据会覆盖defaults中的同名数据)

      attributes = _.extend({}, defaults, attributes);

    }

    // 显式指定模型所属的Collection对象(在调用Collection的add, push等将模型添加到集合中的方法时, 会自动设置模型所属的Collection对象)

    if(options && options.collection)

      this.collection = options.collection;

    // attributes属性存储了当前模型的JSON对象化数据, 创建模型时默认为空

    this.attributes = {};

    // 定义_escapedAttributes缓存对象, 它将缓存通过escape方法处理过的数据

    this._escapedAttributes = {};

    // 为每一个模型配置一个唯一标识

    this.cid = _.uniqueId('c');

    // 定义一系列用于记录数据状态的对象, 具体含义请参考对象定义时的注释

    this.changed = {};

    this._silent = {};

    this._pending = {};

    // 创建实例时设置初始化数据, 首次设置使用silent参数, 不会触发change事件

    this.set(attributes, {

      silent : true

    });

    // 上面已经设置了初始化数据, changed, _silent, _pending对象的状态可能已经发生变化, 这里重新进行初始化

    this.changed = {};

    this._silent = {};

    this._pending = {};

    // _previousAttributes变量存储模型数据的一个副本

    // 用于在change事件中获取模型数据被改变之前的状态, 可通过previous或previousAttributes方法获取上一个状态的数据

    this._previousAttributes = _.clone(this.attributes);

    // 调用initialize初始化方法

    this.initialize.apply(this, arguments);

  };

  // 使用extend方法为Model原型定义一系列属性和方法

  _.extend(Model.prototype, Events, {

 

    // changed属性记录了每次调用set方法时, 被改变数据的key集合

    changed : null,

 

    // // 当指定silent属性时, 不会触发change事件, 被改变的数据会记录下来, 直到下一次触发change事件

    // _silent属性用来记录使用silent时的被改变的数据

    _silent : null,

 

    _pending : null,

 

    // 每个模型的唯一标识属性(默认为"id", 通过修改idAttribute可自定义id属性名)

    // 如果在设置数据时包含了id属性, 则id将会覆盖模型的id

    // id用于在Collection集合中查找和标识模型, 与后台接口通信时也会以id作为一条记录的标识

    idAttribute : 'id',

 

    // 模型初始化方法, 在模型被构造结束后自动调用

    initialize : function() {

    },

    // 返回当前模型中数据的一个副本(JSON对象格式)

    toJSON : function(options) {

      return _.clone(this.attributes);

    },

    // 根据attr属性名, 获取模型中的数据值

    get : function(attr) {

      return this.attributes[attr];

    },

    // 根据attr属性名, 获取模型中的数据值, 数据值包含的HTML特殊字符将被转换为HTML实体, 包含 & < > " ' 
    // 通过 _.escape方法实现

    escape : function(attr) {

      var html;

      // 从_escapedAttributes缓存对象中查找数据, 如果数据已经被缓存则直接返回

      if( html = this._escapedAttributes[attr])

        return html;

      // _escapedAttributes缓存对象中没有找到数据

      // 则先从模型中获取数据

      var val = this.get(attr);

      // 将数据中的HTML使用 _.escape方法转换为实体, 并缓存到_escapedAttributes对象, 便于下次直接获取

      return this._escapedAttributes[attr] = _.escape(val == null "PATHINFO"模式, 服务器对模型的操作只有一个url, 对于修改和删除操作会在url后追加模型id便于标识

    // 如果在模型中定义了urlRoot, 服务器接口应为[urlRoot/id]形式

    // 如果模型所属的Collection集合定义了url方法或属性, 则使用集合中的url形式: [collection.url/id]

    // 在访问服务器url时会在url后面追加上模型的id, 便于服务器标识一条记录, 因此模型中的id需要与服务器记录对应

    // 如果无法获取模型或集合的url, 将调用urlError方法抛出一个异常

    // 如果服务器接口并没有按照"PATHINFO"方式进行组织, 可以通过重载url方法实现与服务器的无缝交互

    url : function() {

      // 定义服务器对应的url路径

      var base = getValue(this, 'urlRoot') || getValue(this.collection, 'url') || urlError();

      // 如果当前模型是客户端新建的模型, 则不存在id属性, 服务器url直接使用base

      if(this.isNew())

        return base;

      // 如果当前模型具有id属性, 可能是调用了save或destroy方法, 将在base后面追加模型的id

      // 下面将判断base最后一个字符是否是"/", 生成的url格式为[base/id]

      return base + (base.charAt(base.length - 1) == '/' "id", 也可以通过修改idAttribute属性自定义标识

    isNew : function() {

      return this.id == null;

    },

    // 数据被更新时触发change事件绑定的函数

    // 当set方法被调用, 会自动调用change方法, 如果在set方法被调用时指定了silent配置, 则需要手动调用change方法

    change : function(options) {

      // options必须是一个对象

      options || ( options = {});

      // this._changing相关的逻辑有些问题

      // this._changing在方法最后被设置为false, 因此方法上面changing变量的值始终为false(第一次为undefined)

      // 作者的初衷应该是想用该变量标示change方法是否执行完毕, 对于浏览器端单线程的脚本来说没有意义, 因为该方法被执行时会阻塞其它脚本

      // changing获取上一次执行的状态, 如果上一次脚本没有执行完毕, 则值为true

      var changing = this._changing;

      // 开始执行标识, 执行过程中值始终为true, 执行完毕后this._changing被修改为false

      this._changing = true;

 

      // 将非本次改变的数据状态添加到_pending对象中

      for(var attr in this._silent)

      this._pending[attr] = true;

 

      // changes对象包含了当前数据上一次执行change事件至今, 已被改变的所有数据

      // 如果之前使用silent未触发change事件, 则本次会被放到changes对象中

      var changes = _.extend({}, options.changes, this._silent);

      // 重置_silent对象

      this._silent = {};

      // 遍历changes对象, 分别针对每一个属性触发单独的change事件

      for(var attr in changes) {

        // 将Model对象, 属性值, 配置项作为参数以此传递给事件的监听函数

        this.trigger('change:' + attr, this, this.get(attr), options);

      }

 

      // 如果方法处于执行中, 则停止执行

      if(changing)

        return this;

 

      // 触发change事件, 任意数据被改变后, 都会依次触发"change:属性"事件和"change"事件

      while(!_.isEmpty(this._pending)) {

        this._pending = {};

        // 触发change事件, 并将Model实例和配置项作为参数传递给监听函数

        this.trigger('change', this, options);

        // 遍历changed对象中的数据, 并依次将已改变数据的状态从changed中移除

        // 在此之后如果调用hasChanged检查数据状态, 将得到false(未改变)

        for(var attr in this.changed) {

          if(this._pending[attr] || this._silent[attr])

            continue;

          // 移除changed中数据的状态

          delete this.changed[attr];

        }

        // change事件执行完毕, _previousAttributes属性将记录当前模型最新的数据副本

        // 因此如果需要获取数据的上一个状态, 一般只通过在触发的change事件中通过previous或previousAttributes方法获取

        this._previousAttributes = _.clone(this.attributes);

      }

 

      // 执行完毕标识

      this._changing = false;

      return this;

    },

    // 检查某个数据是否在上一次执行change事件后被改变过

    /**

     * 一般在change事件中配合previous或previousAttributes方法使用, 如:

     * if(model.hasChanged('attr')) {

     *   var attrPrev = model.previous('attr');

     * }

     */

    hasChanged : function(attr) {

      if(!arguments.length)

        return !_.isEmpty(this.changed);

      return _.has(this.changed, attr);

    },

    // 获取当前模型中的数据与上一次数据中已经发生变化的数据集合

    // (一般在使用silent属性时没有调用change方法, 因此数据会被临时抱存在changed属性中, 上一次的数据可通过previousAttributes方法获取)

    // 如果传递了diff集合, 将使用上一次模型数据与diff集合中的数据进行比较, 返回不一致的数据集合

    // 如果比较结果中没有差异, 则返回false

    changedAttributes : function(diff) {

      // 如果没有指定diff, 将返回当前模型较上一次状态已改变的数据集合, 这些数据已经被存在changed属性中, 因此返回changed集合的一个副本

      if(!diff)

        return this.hasChanged() "error"事件, 如果在options中指定了error处理函数, 则只会执行options.error函数

    // @param {Object} attrs 数据模型的attributes属性, 存储模型的对象化数据

    // @param {Object} options 配置项

    // @return {Boolean} 验证通过返回true, 不通过返回false

    _validate : function(attrs, options) {

      // 如果在调用set, save, add等数据更新方法时设置了options.silent属性, 则忽略验证

      // 如果Model中没有添加validate方法, 则忽略验证

      if(options.silent || !this.validate)

        return true;

      // 获取对象中所有的属性值, 并放入validate方法中进行验证

      // validate方法包含2个参数, 分别为模型中的数据集合与配置对象, 如果验证通过则不返回任何数据(默认为undefined), 验证失败则返回带有错误信息数据

      attrs = _.extend({}, this.attributes, attrs);

      var error = this.validate(attrs, options);

      // 验证通过

      if(!error)

        return true;

      // 验证未通过

      // 如果配置对象中设置了error错误处理方法, 则调用该方法并将错误数据和配置对象传递给该方法

      if(options && options.error) {

        options.error(this, error, options);

      } else {

        // 如果对模型绑定了error事件监听, 则触发绑定事件

        this.trigger('error', this, error, options);

      }

      // 返回验证未通过标识

      return false;

    }

  });

 

  // Backbone.Collection 数据模型集合相关

  // -------------------

 

  // Collection集合存储一系列相同类的数据模型, 并提供相关方法对模型进行操作

  var Collection = Backbone.Collection = function(models, options) {

    // 配置对象

    options || ( options = {});

    // 在配置参数中设置集合的模型类

    if(options.model)

      this.model = options.model;

    // 如果设置了comparator属性, 则集合中的数据将按照comparator方法中的排序算法进行排序(在add方法中会自动调用)

    if(options.comparator)

      this.comparator = options.comparator;

    // 实例化时重置集合的内部状态(第一次调用时可理解为定义状态)

    this._reset();

    // 调用自定义初始化方法, 如果需要一般会重载initialize方法

    this.initialize.apply(this, arguments);

    // 如果指定了models数据, 则调用reset方法将数据添加到集合中

    // 首次调用时设置了silent参数, 因此不会触发"reset"事件

    if(models)

      this.reset(models, {

        silent : true,

        parse : options.parse

      });

  };

  // 通过extend方法定义集合类原型方法

  _.extend(Collection.prototype, Events, {

 

    // 定义集合的模型类, 模型类必须是一个Backbone.Model的子类

    // 在使用集合相关方法(如add, create等)时, 允许传入数据对象, 集合方法会根据定义的模型类自动创建对应的实例

    // 集合中存储的数据模型应该都是同一个模型类的实例

    model : Model,

 

    // 初始化方法, 该方法在集合实例被创建后自动调用

    // 一般会在定义集合类时重载该方法

    initialize : function() {

    },

    // 返回一个数组, 包含了集合中每个模型的数据对象

    toJSON : function(options) {

      // 通过Undersocre的map方法将集合中每一个模型的toJSON结果组成一个数组, 并返回

      return this.map(function(model) {

        // 依次调用每个模型对象的toJSON方法, 该方法默认将返回模型的数据对象(复制的副本)

        // 如果需要返回字符串等其它形式, 可以重载toJSON方法

        return model.toJSON(options);

      });

    },

    // 向集合中添加一个或多个模型对象

    // 默认会触发"add"事件, 如果在options中设置了silent属性, 可以关闭此次事件触发

    // 传入的models可以是一个或一系列的模型对象(Model类的实例), 如果在集合中设置了model属性, 则允许直接传入数据对象(如 {name: 'test'}), 将自动将数据对象实例化为model指向的模型对象

    add : function(models, options) {

      // 局部变量定义

      var i, index, length, model, cid, id, cids = {}, ids = {}, dups = [];

      options || ( options = {});

      // models必须是一个数组, 如果只传入了一个模型, 则将其转换为数组

      models = _.isArray(models) "Can't add an invalid model to a collection");

        }

        // 当前模型的cid和id

        cid = model.cid;

        id = model.id;

        // dups数组中记录了无效或重复的模型索引(models数组中的索引), 并在下一步进行过滤删除

        // 如果cids, ids变量中已经存在了该模型的索引, 则认为是同一个模型在传入的models数组中声明了多次

        // 如果_byCid, _byId对象中已经存在了该模型的索引, 则认为同一个模型在当前集合中已经存在

        // 对于上述两种情况, 将模型的索引记录到dups进行过滤删除

        if(cids[cid] || this._byCid[cid] || ((id != null) && (ids[id] || this._byId[id]))) {

          dups.push(i);

          continue;

        }

        // 将models中已经遍历过的模型记录下来, 用于在下一次循环时进行重复检查

        cids[cid] = ids[id] = model;

      }

 

      // 从models中删除无效或重复的模型, 保留目前集合中真正需要添加的模型列表

      i = dups.length;

      while(i--) {

        models.splice(dups[i], 1);

      }

 

      // 遍历需要添加的模型, 监听模型事件并记录_byCid, _byId列表, 用于在调用get和getByCid方法时作为索引

      for( i = 0, length = models.length; i < length; i++) {

        // 监听模型中的所有事件, 并执行_onModelEvent方法

        // _onModelEvent方法中会对模型抛出的add, remove, destroy和change事件进行处理, 以便模型与集合中的状态保持同步

        ( model = models[i]).on('all', this._onModelEvent, this);

        // 将模型根据cid记录到_byCid对象, 便于根据cid进行查找

        this._byCid[model.cid] = model;

        // 将模型根据id记录到_byId对象, 便于根据id进行查找

        if(model.id != null)

          this._byId[model.id] = model;

      }

 

      // 改变集合的length属性, length属性记录了当前集合中模型的数量

      this.length += length;

      // 设置新模型列表插入到集合中的位置, 如果在options中设置了at参数, 则在集合的at位置插入

      // 默认将插入到集合的末尾

      // 如果设置了comparator自定义排序方法, 则设置at后还将按照comparator中的方法进行排序, 因此最终的顺序可能并非在at指定的位置

      index = options.at != null "add"事件, 如果设置了silent属性, 则阻止事件触发

      if(options.silent)

        return this;

      // 遍历新增加的模型列表

      for( i = 0, length = this.models.length; i < length; i++) {

        if(!cids[( model = this.models[i]).cid])

          continue;

        options.index = i;

        // 触发模型的"add"事件, 因为集合监听了模型的"all"事件, 因此在_onModelEvent方法中, 集合也将触发"add"事件

        // 详细信息可参考Collection.prototype._onModelEvent方法

        model.trigger('add', model, this, options);

      }

      return this;

    },

    // 从集合中移除模型对象(支持移除多个模型)

    // 传入的models可以是需要移除的模型对象, 或模型的cid和模型的id

    // 移除模型并不会调用模型的destroy方法

    // 如果没有设置options.silent参数, 将触发模型的remove事件, 同时将触发集合的remove事件(集合通过_onModelEvent方法监听了模型的所有事件)

    remove : function(models, options) {

      var i, l, index, model;

      // options默认为空对象

      options || ( options = {});

      // models必须是数组类型, 当只移除一个模型时, 将其放入一个数组

      models = _.isArray(models) "Jack"的模型(数组)

    where : function(attrs) {

      // attrs不能为空值

      if(_.isEmpty(attrs))

        return [];

      // 通过filter方法对集合中的模型进行筛选

      // filter方法是Underscore中的方法, 用于将遍历集合中的元素, 并将能通过处理器验证(返回值为true)的元素作为数组返回

      return this.filter(function(model) {

        // 遍历attrs对象中的验证规则

        for(var key in attrs) {

          // 将attrs中的验证规则与集合中的模型进行匹配

          if(attrs[key] !== model.get(key))

            return false;

        }

        return true;

      });

    },

    // 对集合中的模型按照comparator属性指定的方法进行排序

    // 如果没有在options中设置silent参数, 则排序后将触发reset事件

    sort : function(options) {

      // options默认是一个对象

      options || ( options = {});

      // 调用sort方法必须指定了comparator属性(排序算法方法), 否则将抛出一个错误

      if(!this.comparator)

        throw new Error('Cannot sort a set without a comparator');

      // boundComparator存储了绑定当前集合上下文对象的comparator排序算法方法

      var boundComparator = _.bind(this.comparator, this);

      if(this.comparator.length == 1) {

        this.models = this.sortBy(boundComparator);

      } else {

        // 调用Array.prototype.sort通过comparator算法对数据进行自定义排序

        this.models.sort(boundComparator);

      }

      // 如果没有指定silent参数, 则触发reset事件

      if(!options.silent)

        this.trigger('reset', this, options);

      return this;

    },

    // 将集合中所有模型的attr属性值存放到一个数组并返回

    pluck : function(attr) {

      // map是Underscore中的方法, 用于遍历一个集合, 并将所有处理器的返回值作为一个数组返回

      return _.map(this.models, function(model) {

        // 返回当前模型的attr属性值

        return model.get(attr);

      });

    },

    // 替换集合中的所有模型数据(models)

    // 该操作将删除集合中当前的所有数据和状态, 并重新将数据设置为models

    // models应该是一个数组, 可以包含一系列Model模型对象, 或原始对象(将在add方法中自动创建为模型对象)

    reset : function(models, options) {

      // models是进行替换的模型(或数据)数组

      models || ( models = []);

      // options默认是一个空对象

      options || ( options = {});

      // 遍历当前集合中的模型, 依次删除并解除它们与集合的引用关系

      for(var i = 0, l = this.models.length; i < l; i++) {

        this._removeReference(this.models[i]);

      }

      // 删除集合数据并重置状态

      this._reset();

      // 通过add方法将新的模型数据添加到集合

      // 这里通过exnted方法将配置项覆盖到一个新的对象, 该对象默认silent为true, 因此不会触发"add"事件

      // 如果在调用reset方法时没有设置silent属性则会触发reset事件, 如果设置为true则不会触发任何事件, 如果设置为false, 将依次触发"add"和"reset"事件

      this.add(models, _.extend({

        silent : true

      }, options));

      // 如果在调用reset方法时没有设置silent属性, 则触发reset事件

      if(!options.silent)

        this.trigger('reset', this, options);

      return this;

    },

    // 从服务器获取集合的初始化数据

    // 如果在options中设置参数add=true, 则获取到的数据会被追加到集合中, 否则将以服务器返回的数据替换集合中的当前数据

    fetch : function(options) {

      // 复制options对象, 因为options对象在后面会被修改用于临时存储数据

      options = options "add"事件, 同时也会在此方法中触发集合的"add"事件)

      // 这对于监听并处理集合中模型状态的变化非常有效

      // 在监听的集合事件中, 触发对应事件的模型会被作为参数传递给集合的监听函数

      this.trigger.apply(this, arguments);

    }

  });

 

  // 定义Underscore中的集合操作的相关方法

  // 将Underscore中一系列集合操作方法复制到Collection集合类的原型对象中

  // 这样就可以直接通过集合对象调用Underscore相关的集合方法

  // 这些方法在调用时所操作的集合数据是当前Collection对象的models数据

  var methods = ['forEach', 'each', 'map', 'reduce', 'reduceRight', 'find', 'detect', 'filter', 'select', 'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', 'max', 'min', 'sortBy', 'sortedIndex', 'toArray', 'size', 'first', 'initial', 'rest', 'last', 'without', 'indexOf', 'shuffle', 'lastIndexOf', 'isEmpty', 'groupBy'];

 

  // 遍历已经定义的方法列表

  _.each(methods, function(method) {

    // 将方法复制到Collection集合类的原型对象

    Collection.prototype[method] = function() {

      // 调用时直接使用Underscore的方法, 上下文对象保持为Underscore对象

      // 需要注意的是这里传递给Underscore方法的集合参数是 this.models, 因此在使用这些方法时, 所操作的集合对象是当前Collection对象的models数据

      return _[method].apply(_, [this.models].concat(_.toArray(arguments)));

    };

  });

  // Backbone.Router URL路由器

  // -------------------

 

  // 通过继承Backbone.Router类实现自定义的路由器

  // 路由器允许定义路由规则, 通过URL片段进行导航, 并将每一个规则对应到一个方法, 当URL匹配某个规则时会自动执行该方法

  // 路由器通过URL进行导航, 导航方式分为pushState, Hash, 和监听方式(详细可参考Backbone.History类)

  // 在创建Router实例时, 通过options.routes来设置某个路由规则对应的监听方法

  // options.routes中的路由规则按照 {规则名称: 方法名称}进行组织, 每一个路由规则所对应的方法, 都必须是在Router实例中的已经声明的方法

  // options.routes定义的路由规则按照先后顺序进行匹配, 如果当前URL能被多个规则匹配, 则只会执行第一个匹配的事件方法

  var Router = Backbone.Router = function(options) {

    // options默认是一个空对象

    options || ( options = {});

    // 如果在options中设置了routes对象(路由规则), 则赋给当前实例的routes属性

    // routes属性记录了路由规则与事件方法的绑定关系, 当URL与某一个规则匹配时, 会自动调用关联的事件方法

    if(options.routes)

      this.routes = options.routes;

    // 解析和绑定路由规则

    this._bindRoutes();

    // 调用自定义的初始化方法

    this.initialize.apply(this, arguments);

  };

  // 定义用于将字符串形式的路由规则, 转换为可执行的正则表达式规则时的查找条件

  // (字符串形式的路由规则, 通过\w+进行匹配, 因此只支持字母数字和下划线组成的字符串)

  // 匹配一个URL片段中(以/"斜线"为分隔)的动态路由规则

  // 如: (topic/:id) 匹配 (topic/1228), 监听事件function(id) { // id为1228 }

  var namedParam = /:\w+/g;

  // 匹配整个URL片段中的动态路由规则

  // 如: (topic*id) 匹配 (url#/topic1228), 监听事件function(id) { // id为1228 }

  var splatParam = /\*\w+/g;

  // 匹配URL片段中的特殊字符, 并在字符前加上转义符, 防止特殊字符在被转换为正则表达式后变成元字符

  // 如: (abc)^[,.] 将被转换为 \(abc\)\^\[\,\.\]

  var escapeRegExp = /[-[\]{}()+"斜线"为分隔的动态路由规则转换为([^\/]+), 在正则中表示以/"斜线"开头的多个字符

      // 将字符串中的*"星号"动态路由规则转换为(.*"order"和"123"作为参数传递给事件方法 )

      // 请注意namedParam和splatParam替换后的正则表达式都是用()括号将匹配的内容包含起来, 这是为了方便取出匹配的内容作为参数传递给事件方法

      // 请注意namedParam和splatParam匹配的字符串 :str, *str中的str字符串是无意义的, 它们会在下面替换后被忽略, 但一般写作和监听事件方法的参数同名, 以便进行标识

      route = route.replace(escapeRegExp, '\\$&').replace(namedParam, '([^\/]+)').replace(splatParam, '(.*"teams/35/1228", "35", "1228"]

     * 数组中的一个元素是URL片段字符串本身, 从第二个开始则依次为路由规则表达式中的参数

     */

    _extractParameters : function(route, fragment) {

      return route.exec(fragment).slice(1);

    }

  });

 

  // Backbone.History 路由器管理

  // ----------------

 

  // History类提供路由管理相关操作, 包括监听URL的变化, (通过popstate和onhashchange事件进行监听, 对于不支持事件的浏览器通过setInterval心跳监控)

  // 提供路由规则与当前URL的匹配验证, 和触发相关的监听事件

  // History一般不会被直接调用, 在第一次实例化Router对象时, 将自动创建一个History的单例(通过Backbone.history访问)

  var History = Backbone.History = function() {

    // handlers属性记录了当前所有路由对象中已经设置的规则和监听列表

    // 形式如: [{route: route, callback: callback}], route记录了正则表达式规则, callback记录了匹配规则时的监听事件

    // 当history对象监听到URL发生变化时, 会自动与handlers中定义的规则进行匹配, 并调用监听事件

    this.handlers = [];

    // 将checkUrl方法的上下文对象绑定到history对象, 因为checkUrl方法被作为popstate和onhashchange事件或setInterval的回调函数, 在执行回调时, 上下文对象会被改变

    // checkUrl方法用于在监听到URL发生变化时检查并调用loadUrl方法

    _.bindAll(this, 'checkUrl');

  };

  // 定义用于匹配URL片段中首字符是否为"#"或"/"的正则

  var routeStripper = /^[#\/]/;

 

  // 定义用于匹配从userAgent中获取的字符串是否包含IE浏览器的标识, 用于判断当前浏览器是否为IE

  var isExplorer = /msie [\w.]+/;

 

  // 记录当前history单例对象是否已经被初始化过(调用start方法)

  History.started = false;

 

  // 向History类的原型对象中添加方法, 这些方法可以通过History的实例调用(即Backbone.history对象)

  _.extend(History.prototype, Events, {

 

    // 当用户使用低版本的IE浏览器(不支持onhashchange事件)时, 通过心跳监听路由状态的变化

    // interval属性设置心跳频率(毫秒), 该频率如果太低可能会导致延迟, 如果太高可能会消耗CPU资源(需要考虑用户使用低端浏览器时的设备配置)

    interval : 50,

 

    // 获取location中Hash字符串(锚点#后的片段)

    getHash : function(windowOverride) {

      // 如果传入了一个window对象, 则从该对象中获取, 否则默认从当前window对象中获取

      var loc = windowOverride "#"或"/", 则去除该字符

      // 返回处理之后的URL片段

      return fragment.replace(routeStripper, '');

    },

    // 初始化History实例, 该方法只会被调用一次, 应该在创建并初始化Router对象之后被自动调用

    // 该方法作为整个路由的调度器, 它将针对不同浏览器监听URL片段的变化, 负责验证并通知到监听函数

    start : function(options) {

      // 如果history对象已经被初始化过, 则抛出错误

      if(History.started)

        throw new Error("Backbone.history has already been started");

      // 设置history对象的初始化状态

      History.started = true;

 

      // 设置配置项, 使用调用start方法时传递的options配置项覆盖默认配置

      this.options = _.extend({}, {

        // root属性设置URL导航中的路由根目录

        // 如果使用pushState方式进行路由, 则root目录之后的地址会根据不同的路由产生不同的地址(这可能会定位到不同的页面, 因此需要确保服务器支持)

        // 如果使用Hash锚点的方式进行路由, 则root表示URL后锚点(#)的位置

        root : '/'

      }, this.options, options);

      /**

       * history针对不同浏览器特性, 实现了3种方式的监听:

       * - 对于支持HTML5中popstate事件的浏览器, 通过popstate事件进行监听

       * - 对于不支持popstate的浏览器, 使用onhashchange事件进行监听(通过改变hash(锚点)设置的URL在被载入时会触发onhashchange事件)

       * - 对于不支持popstate和onhashchange事件的浏览器, 通过保持心跳监听

       *

       * 关于HTML5中popstate事件的相关方法:

       * - pushState可以将指定的URL添加一个新的history实体到浏览器历史里

       * - replaceState方法可以将当前的history实体替换为指定的URL

       * 使用pushState和replaceState方法时仅替换当前URL, 而并不会真正转到这个URL(当使用后退或前进按钮时, 也不会跳转到该URL)

       * (这两个方法可以解决在AJAX单页应用中浏览器前进, 后退操作的问题)

       * 当使用pushState或replaceState方法替换的URL, 在被载入时会触发onpopstate事件

       * 浏览器支持情况:

       * Chrome 5, Firefox 4.0, IE 10, Opera 11.5, Safari 5.0

       *

       * 注意:

       * - history.start方法默认使用Hash方式进行导航

       * - 如果需要启用pushState方式进行导航, 需要在调用start方法时, 手动传入配置options.pushState

       *  (设置前请确保浏览器支持pushState特性, 否则将默认转换为Hash方式)

       * - 当使用pushState方式进行导航时, URL可能会从options.root指定的根目录后发生变化, 这可能会导航到不同页面, 因此请确保服务器已经支持pushState方式的导航

       */

      // _wantsHashChange属性记录是否希望使用hash(锚点)的方式来记录和导航路由器

      // 除非在options配置项中手动设置hashChange为false, 否则默认将使用hash锚点的方式

      // (如果手动设置了options.pushState为true, 且浏览器支持pushState特性, 则会使用pushState方式)

      this._wantsHashChange = this.options.hashChange !== false;

      // _wantsPushState属性记录是否希望使用pushState方式来记录和导航路由器

      // pushState是HTML5中为window.history添加的新特性, 如果没有手动声明options.pushState为true, 则默认将使用hash方式

      this._wantsPushState = !!this.options.pushState;

      // _hasPushState属性记录浏览器是否支持pushState特性

      // 如果在options中设置了pushState(即希望使用pushState方式), 则检查浏览器是否支持该特性

      this._hasPushState = !!(this.options.pushState && window.history && window.history.pushState);

      // 获取当前URL中的路由字符串

      var fragment = this.getFragment();

      // documentMode是IE浏览器的独有属性, 用于标识当前浏览器使用的渲染模式

      var docMode = document.documentMode;

      // oldIE用于检查当前浏览器是否为低版本的IE浏览器(即IE 7.0以下版本)

      // 这句代码可理解为: 当前浏览器为IE, 但不支持documentMode属性, 或documentMode属性返回的渲染模式为IE7.0以下

      var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7));

 

      if(oldIE) {

        // 如果用户使用低版本的IE浏览器, 不支持popstate和onhashchange事件

        // 向DOM中插入一个隐藏的iframe, 并通过改变和心跳监听该iframe的URL实现路由

        this.iframe = $('<iframe src="/UploadFiles/2021-04-02/javascript:0">
标签:
Backbone.js,源码注释,中文

幽灵资源网 Design By www.bzswh.com
广告合作:本站广告合作请联系QQ:858582 申请时备注:广告合作(否则不回)
免责声明:本站文章均来自网站采集或用户投稿,网站不提供任何软件下载或自行开发的软件! 如有用户或公司发现本站内容信息存在侵权行为,请邮件告知! 858582#qq.com
幽灵资源网 Design By www.bzswh.com

《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线

暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。

艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。

《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。