亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

Backbone.js 0.9.2 源碼注釋中文翻譯版

2019-11-20 12:09:40
字體:
來源:轉載
供稿:網友
// 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 ? list.tail : {};        node.next = tail = {};        // 記錄本次回調的函數體及上下文信息        node.context = context;        node.callback = callback;        // 重新組裝當前事件的回調列表, 列表中已經加入了本次回調事件        calls[event] = {          tail : tail,          next : list ? list.next : node        };      }      // 返回當前對象, 方便進行方法鏈調用      return this;    },    // 移除對象中已綁定的事件或回調函數, 可以通過events, callback和context對需要刪除的事件或回調函數進行過濾    // - 如果context為空, 則移除所有的callback指定的函數    // - 如果callback為空, 則移除事件中所有的回調函數    // - 如果events為空, 但指定了callback或context, 則移除callback或context指定的回調函數(不區分事件名稱)    // - 如果沒有傳遞任何參數, 則移除對象中綁定的所有事件和回調函數    off : function(events, callback, context) {      var event, calls, node, tail, cb, ctx;       // No events, or removing *all* events.      // 當前對象沒有綁定任何事件      if(!( calls = this._callbacks))        return;      // 如果沒有指定任何參數, 則移除所有事件和回調函數(刪除_callbacks屬性)      if(!(events || callback || context)) {        delete this._callbacks;        return this;      }       // 解析需要移除的事件列表      // - 如果指定了events, 則按照eventSplitter對事件名進行解析      // - 如果沒有指定events, 則解析已綁定所有事件的名稱列表      events = events ? events.split(eventSplitter) : _.keys(calls);       // 循環事件名列表      while( event = events.shift()) {        // 將當前事件對象從列表中移除, 并緩存到node變量中        node = calls[event];        delete calls[event];        // 如果不存在當前事件對象(或沒有指定移除過濾條件, 則認為將移除當前事件及所有回調函數), 則終止此次操作(事件對象在上一步已經移除)        if(!node || !(callback || context))          continue;        // Create a new list, omitting the indicated callbacks.        // 根據回調函數或上下文過濾條件, 組裝一個新的事件對象并重新綁定        tail = node.tail;        // 遍歷事件中的所有回調對象        while(( node = node.next) !== tail) {          cb = node.callback;          ctx = node.context;          // 根據參數中的回調函數和上下文, 對回調函數進行過濾, 將不符合過濾條件的回調函數重新綁定到事件中(因為事件中的所有回調函數在上面已經被移除)          if((callback && cb !== callback) || (context && ctx !== context)) {            this.on(event, cb, ctx);          }        }      }       return this;    },    // 觸發已經定義的一個或多個事件, 依次執行綁定的回調函數列表    trigger : function(events) {      var event, node, calls, tail, args, all, rest;      // 當前對象沒有綁定任何事件      if(!( calls = this._callbacks))        return this;      // 獲取回調函數列表中綁定的"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 ? '' : '' + val);    },    // 檢查模型中是否存在某個屬性, 當該屬性的值被轉換為Boolean類型后值為false, 則認為不存在    // 如果值為false, null, undefined, 0, NaN, 或空字符串時, 均會被轉換為false    has : function(attr) {      return this.get(attr) != null;    },    // 設置模型中的數據, 如果key值不存在, 則作為新的屬性添加到模型, 如果key值已經存在, 則修改為新的值    set : function(key, value, options) {      // attrs變量中記錄需要設置的數據對象      var attrs, attr, val;       // 參數形式允許key-value對象形式, 或通過key, value兩個參數進行單獨設置      // 如果key是一個對象, 則認定為使用對象形式設置, 第二個參數將被視為options參數      if(_.isObject(key) || key == null) {        attrs = key;        options = value;      } else {        // 通過key, value兩個參數單獨設置, 將數據放到attrs對象中方便統一處理        attrs = {};        attrs[key] = value;      }       // options配置項必須是一個對象, 如果沒有設置options則默認值為一個空對象      options || ( options = {});      // 沒有設置參數時不執行任何動作      if(!attrs)        return this;      // 如果被設置的數據對象屬于Model類的一個實例, 則將Model對象的attributes數據對象賦給attrs      // 一般在復制一個Model對象的數據到另一個Model對象時, 會執行該動作      if( attrs instanceof Model)        attrs = attrs.attributes;      // 如果options配置對象中設置了unset屬性, 則將attrs數據對象中的所有屬性重置為undefined      // 一般在復制一個Model對象的數據到另一個Model對象時, 但僅僅需要復制Model中的數據而不需要復制值時執行該操作      if(options.unset)        for(attr in attrs)        attrs[attr] =        void 0;       // 對當前數據進行驗證, 如果驗證未通過則停止執行      if(!this._validate(attrs, options))        return false;       // 如果設置的id屬性名被包含在數據集合中, 則將id覆蓋到模型的id屬性      // 這是為了確保在自定義id屬性名后, 訪問模型的id屬性時, 也能正確訪問到id      if(this.idAttribute in attrs)        this.id = attrs[this.idAttribute];       var changes = options.changes = {};      // now記錄當前模型中的數據對象      var now = this.attributes;      // escaped記錄當前模型中通過escape緩存過的數據      var escaped = this._escapedAttributes;      // prev記錄模型中數據被改變之前的值      var prev = this._previousAttributes || {};       // 遍歷需要設置的數據對象      for(attr in attrs) {        // attr存儲當前屬性名稱, val存儲當前屬性的值        val = attrs[attr];         // 如果當前數據在模型中不存在, 或已經發生變化, 或在options中指定了unset屬性刪除, 則刪除該數據被換存在_escapedAttributes中的數據        if(!_.isEqual(now[attr], val) || (options.unset && _.has(now, attr))) {          // 僅刪除通過escape緩存過的數據, 這是為了保證緩存中的數據與模型中的真實數據保持同步          delete escaped[attr];          // 如果指定了silent屬性, 則此次set方法調用不會觸發change事件, 因此將被改變的數據記錄到_silent屬性中, 便于下一次觸發change事件時, 通知事件監聽函數此數據已經改變          // 如果沒有指定silent屬性, 則直接設置changes屬性中當前數據為已改變狀態          (options.silent ? this._silent : changes)[attr] = true;        }         // 如果在options中設置了unset, 則從模型中刪除該數據(包括key)        // 如果沒有指定unset屬性, 則認為將新增或修改數據, 向模型的數據對象中加入新的數據        options.unset ?        delete now[attr] : now[attr] = val;         // 如果模型中的數據與新的數據不一致, 則表示該數據已發生變化        if(!_.isEqual(prev[attr], val) || (_.has(now, attr) != _.has(prev, attr))) {          // 在changed屬性中記錄當前屬性已經發生變化的狀態          this.changed[attr] = val;          if(!options.silent)            this._pending[attr] = true;        } else {          // 如果數據沒有發生變化, 則從changed屬性中移除已變化狀態          delete this.changed[attr];          delete this._pending[attr];        }      }       // 調用change方法, 將觸發change事件綁定的函數      if(!options.silent)        this.change(options);      return this;    },    // 從當前模型中刪除指定的數據(屬性也將被同時刪除)    unset : function(attr, options) {      (options || ( options = {})).unset = true;      // 通過options.unset配置項告知set方法進行刪除操作      return this.set(attr, null, options);    },    // 清除當前模型中的所有數據和屬性    clear : function(options) {      (options || ( options = {})).unset = true;      // 克隆一個當前模型的屬性副本, 并通過options.unset配置項告知set方法執行刪除操作      return this.set(_.clone(this.attributes), options);    },    // 從服務器獲取默認的模型數據, 獲取數據后使用set方法將數據填充到模型, 因此如果獲取到的數據與當前模型中的數據不一致, 將會觸發change事件    fetch : function(options) {      // 確保options是一個新的對象, 隨后將改變options中的屬性      options = options ? _.clone(options) : {};      var model = this;      // 在options中可以指定獲取數據成功后的自定義回調函數      var success = options.success;      // 當獲取數據成功后填充數據并調用自定義成功回調函數      options.success = function(resp, status, xhr) {        // 通過parse方法將服務器返回的數據進行轉換        // 通過set方法將轉換后的數據填充到模型中, 因此可能會觸發change事件(當數據發生變化時)        // 如果填充數據時驗證失敗, 則不會調用自定義success回調函數        if(!model.set(model.parse(resp, xhr), options))          return false;        // 調用自定義的success回調函數        if(success)          success(model, resp);      };      // 請求發生錯誤時通過wrapError處理error事件      options.error = Backbone.wrapError(options.error, model, options);      // 調用sync方法從服務器獲取數據      return (this.sync || Backbone.sync).call(this, 'read', this, options);    },    // 保存模型中的數據到服務器    save : function(key, value, options) {      // attrs存儲需要保存到服務器的數據對象      var attrs, current;       // 支持設置單個屬性的方式 key: value      // 支持對象形式的批量設置方式 {key: value}      if(_.isObject(key) || key == null) {        // 如果key是一個對象, 則認為是通過對象方式設置        // 此時第二個參數被認為是options        attrs = key;        options = value;      } else {        // 如果是通過key: value形式設置單個屬性, 則直接設置attrs        attrs = {};        attrs[key] = value;      }      // 配置對象必須是一個新的對象      options = options ? _.clone(options) : {};       // 如果在options中設置了wait選項, 則被改變的數據將會被提前驗證, 且服務器沒有響應新數據(或響應失敗)時, 本地數據會被還原為修改前的狀態      // 如果沒有設置wait選項, 則無論服務器是否設置成功, 本地數據均會被修改為最新狀態      if(options.wait) {        // 對需要保存的數據提前進行驗證        if(!this._validate(attrs, options))          return false;        // 記錄當前模型中的數據, 用于在將數據發送到服務器后, 將數據進行還原        // 如果服務器響應失敗或沒有返回數據, 則可以保持修改前的狀態        current = _.clone(this.attributes);      }       // silentOptions在options對象中加入了silent(不對數據進行驗證)      // 當使用wait參數時使用silentOptions配置項, 因為在上面已經對數據進行過驗證      // 如果沒有設置wait參數, 則仍然使用原始的options配置項      var silentOptions = _.extend({}, options, {        silent : true      });      // 將修改過最新的數據保存到模型中, 便于在sync方法中獲取模型數據保存到服務器      if(attrs && !this.set(attrs, options.wait ? silentOptions : options)) {        return false;      }       var model = this;      // 在options中可以指定保存數據成功后的自定義回調函數      var success = options.success;      // 服務器響應成功后執行success      options.success = function(resp, status, xhr) {        // 獲取服務器響應最新狀態的數據        var serverAttrs = model.parse(resp, xhr);        // 如果使用了wait參數, 則優先將修改后的數據狀態直接設置到模型        if(options.wait) {          delete options.wait;          serverAttrs = _.extend(attrs || {}, serverAttrs);        }        // 將最新的數據狀態設置到模型中        // 如果調用set方法時驗證失敗, 則不會調用自定義的success回調函數        if(!model.set(serverAttrs, options))          return false;        if(success) {          // 調用響應成功后自定義的success回調函數          success(model, resp);        } else {          // 如果沒有指定自定義回調, 則默認觸發sync事件          model.trigger('sync', model, resp, options);        }      };      // 請求發生錯誤時通過wrapError處理error事件      options.error = Backbone.wrapError(options.error, model, options);      // 將模型中的數據保存到服務器      // 如果當前模型是一個新建的模型(沒有id), 則使用create方法(新增), 否則認為是update方法(修改)      var method = this.isNew() ? 'create' : 'update';      var xhr = (this.sync || Backbone.sync).call(this, method, this, options);      // 如果設置了options.wait, 則將數據還原為修改前的狀態      // 此時保存的請求還沒有得到響應, 因此如果響應失敗, 模型中將保持修改前的狀態, 如果服務器響應成功, 則會在success中設置模型中的數據為最新狀態      if(options.wait)        this.set(current, silentOptions);      return xhr;    },    // 刪除模型, 模型將同時從所屬的Collection集合中被刪除    // 如果模型是在客戶端新建的, 則直接從客戶端刪除    // 如果模型數據同時存在服務器, 則同時會刪除服務器端的數據    destroy : function(options) {      // 配置項必須是一個新的對象      options = options ? _.clone(options) : {};      var model = this;      // 在options中可以指定刪除數據成功后的自定義回調函數      var success = options.success;      // 刪除數據成功調用, 觸發destroy事件, 如果模型存在于Collection集合中, 集合將監聽destroy事件并在觸發時從集合中移除該模型      // 刪除模型時, 模型中的數據并沒有被清空, 但模型已經從集合中移除, 因此當沒有任何地方引用該模型時, 會被自動從內存中釋放      // 建議在刪除模型時, 將模型對象的引用變量設置為null      var triggerDestroy = function() {        model.trigger('destroy', model, model.collection, options);      };      // 如果該模型是一個客戶端新建的模型, 則直接調用triggerDestroy從集合中將模型移除      if(this.isNew()) {        triggerDestroy();        return false;      }       // 當從服務器刪除數據成功時      options.success = function(resp) {        // 如果在options對象中配置wait項, 則表示本地內存中的模型數據, 會在服務器數據被刪除成功后再刪除        // 如果服務器響應失敗, 則本地數據不會被刪除        if(options.wait)          triggerDestroy();        if(success) {          // 調用自定義的成功回調函數          success(model, resp);        } else {          // 如果沒有自定義回調, 則默認觸發sync事件          model.trigger('sync', model, resp, options);        }      };      // 請求發生錯誤時通過wrapError處理error事件      options.error = Backbone.wrapError(options.error, model, options);      // 通過sync方法發送刪除數據的請求      var xhr = (this.sync || Backbone.sync).call(this, 'delete', this, options);      // 如果沒有在options對象中配置wait項, 則會先刪除本地數據, 再發送請求刪除服務器數據      // 此時無論服務器刪除是否成功, 本地模型數據已被刪除      if(!options.wait)        triggerDestroy();      return xhr;    },    // 獲取模型在服務器接口中對應的url, 在調用save, fetch, destroy等與服務器交互的方法時, 將使用該方法獲取url    // 生成的url類似于"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) == '/' ? '' : '/') + encodeURIComponent(this.id);    },    // parse方法用于解析從服務器獲取的數據, 返回一個能夠被set方法解析的模型數據    // 一般parse方法會根據服務器返回的數據進行重載, 以便構建與服務器的無縫連接    // 當服務器返回的數據結構與set方法所需的數據結構不一致(例如服務器返回XML格式數據時), 可使用parse方法進行轉換    parse : function(resp, xhr) {      return resp;    },    // 創建一個新的模型, 它具有和當前模型相同的數據    clone : function() {      return new this.constructor(this.attributes);    },    // 檢查當前模型是否是客戶端創建的新模型    // 檢查方式是根據模型是否存在id標識, 客戶端創建的新模型沒有id標識    // 因此服務器響應的模型數據中必須包含id標識, 標識的屬性名默認為"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() ? _.clone(this.changed) : false;      // 指定了需要進行比較的diff集合, 將返回上一次的數據與diff集合的比較結果      // old變量存儲了上一個狀態的模型數據      var val, changed = false, old = this._previousAttributes;      // 遍歷diff集合, 并將每一項與上一個狀態的集合進行比較      for(var attr in diff) {        // 將比較結果不一致的數據臨時存儲到changed變量        if(_.isEqual(old[attr], ( val = diff[attr])))          continue;        (changed || (changed = {}))[attr] = val;      }      // 返回比較結果      return changed;    },    // 在模型觸發的change事件中, 獲取某個屬性被改變前上一個狀態的數據, 一般用于進行數據比較或回滾    // 該方法一般在change事件中調用, change事件被觸發后, _previousAttributes屬性存放最新的數據    previous : function(attr) {      // attr指定需要獲取上一個狀態的屬性名稱      if(!arguments.length || !this._previousAttributes)        return null;      return this._previousAttributes[attr];    },    // 在模型觸發change事件中, 獲取所有屬性上一個狀態的數據集合    // 該方法類似于previous()方法, 一般在change事件中調用, 用于數據比較或回滾    previousAttributes : function() {      // 將上一個狀態的數據對象克隆為一個新對象并返回      return _.clone(this._previousAttributes);    },    // Check if the model is currently in a valid state. It's only possible to    // get into an *invalid* state if you're using silent changes.    // 驗證當前模型中的數據是否能通過validate方法驗證, 調用前請確保定義了validate方法    isValid : function() {      return !this.validate(this.attributes);    },    // 數據驗證方法, 在調用set, save, add等數據更新方法時, 被自動執行    // 驗證失敗會觸發模型對象的"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) ? models.slice() : [models];       // 遍歷需要添加的模型列表, 遍歷過程中, 將執行以下操作:      // - 將數據對象轉化模型對象      // - 建立模型與集合之間的引用      // - 記錄無效和重復的模型, 并在后面進行過濾      for( i = 0, length = models.length; i < length; i++) {        // 將數據對象轉換為模型對象, 簡歷模型與集合的引用, 并存儲到model(同時models中對應的模型已經被替換為模型對象)        if(!( model = models[i] = this._prepareModel(models[i], options))) {          throw new Error("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 ? options.at : this.models.length;      splice.apply(this.models, [index, 0].concat(models));      // 如果設置了comparator方法, 則將數據按照comparator中的算法進行排序      // 自動排序使用silent屬性阻止觸發reset事件      if(this.comparator)        this.sort({          silent : true        });      // 依次對每個模型對象觸發"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) ? models.slice() : [models];      // 遍歷需要移除的模型列表      for( i = 0, l = models.length; i < l; i++) {        // 所傳入的models列表中可以是需要移除的模型對象, 或模型的cid和模型的id        // (在getByCid和get方法中, 可通過cid, id來獲取模型, 如果傳入的是一個模型對象, 則返回模型本身)        model = this.getByCid(models[i]) || this.get(models[i]);        // 沒有獲取到模型        if(!model)          continue;        // 從_byId列表中移除模型的id引用        delete this._byId[model.id];        // 從_byCid列表中移除模型的cid引用        delete this._byCid[model.cid];        // indexOf是Underscore對象中的方法, 這里通過indexOf方法獲取模型在集合中首次出現的位置        index = this.indexOf(model);        // 從集合列表中移除該模型        this.models.splice(index, 1);        // 重置當前集合的length屬性(記錄集合中模型的數量)        this.length--;        // 如果沒有設置silent屬性, 則觸發模型的remove事件        if(!options.silent) {          // 將當前模型在集合中的位置添加到options對象并傳遞給remove監聽事件, 以便在事件函數中可以使用          options.index = index;          model.trigger('remove', model, this, options);        }        // 解除模型與集合的關系, 包括集合中對模型的引用和事件監聽        this._removeReference(model);      }      return this;    },    // 向集合的末尾添加模型對象    // 如果集合類中定義了comparator排序方法, 則通過push方法添加的模型將按照comparator定義的算法進行排序, 因此模型順序可能會被改變    push : function(model, options) {      // 通過_prepareModel方法將model實例化為模型對象, 這句代碼是多余的, 因為在下面調用的add方法中還會通過_prepareModel獲取一次模型      model = this._prepareModel(model, options);      // 調用add方法將模型添加到集合中(默認添加到集合末尾)      this.add(model, options);      return model;    },    // 移除集合中最后一個模型對象    pop : function(options) {      // 獲取集合中最后一個模型      var model = this.at(this.length - 1);      // 通過remove方法移除該模型      this.remove(model, options);      return model;    },    // 向集合的第一個位置插入模型    // 如果集合類中定義了comparator排序方法, 則通過unshift方法添加的模型將按照comparator定義的算法進行排序, 因此模型順序可能會被改變    unshift : function(model, options) {      // 通過_prepareModel方法將model實例化為模型對象      model = this._prepareModel(model, options);      // 調用add方法將模型插入到集合的第一個位置(設置at為0)      // 如果定義了comparator排序方法, 集合的順序將被重排      this.add(model, _.extend({        at : 0      }, options));      return model;    },    // 移除并返回集合中的第一個模型對象    shift : function(options) {      // 獲得集合中的第一個模型      var model = this.at(0);      // 從集合中刪除該模型      this.remove(model, options);      // 返回模型對象      return model;    },    // 根據id從集合中查找模型并返回    get : function(id) {      if(id == null)        return        void 0;      return this._byId[id.id != null ? id.id : id];    },    // 根據cid從集合中查找模型并返回    getByCid : function(cid) {      return cid && this._byCid[cid.cid || cid];    },    // 根據索引(下標, 從0開始)從集合中查找模型并返回    at : function(index) {      return this.models[index];    },    // 對集合中的模型根據值進行篩選    // attrs是一個篩選對象, 如 {name: 'Jack'}, 將返回集合中所有name為"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 ? _.clone(options) : {};      if(options.parse === undefined)        options.parse = true;      // collection記錄當前集合對象, 用于在success回調函數中使用      var collection = this;      // 自定義回調函數, 數據請求成功后并添加完成后, 會調用自定義success函數      var success = options.success;      // 當從服務器請求數據成功時執行options.success, 該函數中將解析并添加數據      options.success = function(resp, status, xhr) {        // 通過parse方法對服務器返回的數據進行解析, 如果需要自定義數據結構, 可以重載parse方法        // 如果在options中設置add=true, 則調用add方法將數據添加到集合, 否則將通過reset方法將集合中的數據替換為服務器的返回數據        collection[options.add ? 'add' : 'reset'](collection.parse(resp, xhr), options);        // 如果設置了自定義成功回調, 則執行        if(success)          success(collection, resp);      };      // 當服務器返回狀態錯誤時, 通過wrapError方法處理錯誤事件      options.error = Backbone.wrapError(options.error, collection, options);      // 調用Backbone.sync方法發送請求從服務器獲取數據      // 如果需要的數據并不是從服務器獲取, 或獲取方式不使用AJAX, 可以重載Backbone.sync方法      return (this.sync || Backbone.sync).call(this, 'read', this, options);    },    // 向集合中添加并創建一個模型, 同時將該模型保存到服務器    // 如果是通過數據對象來創建模型, 需要在集合中聲明model屬性對應的模型類    // 如果在options中聲明了wait屬性, 則會在服務器創建成功后再將模型添加到集合, 否則先將模型添加到集合, 再保存到服務器(無論保存是否成功)    create : function(model, options) {      var coll = this;      // 定義options對象      options = options ? _.clone(options) : {};      // 通過_prepareModel獲取模型類的實例      model = this._prepareModel(model, options);      // 模型創建失敗      if(!model)        return false;      // 如果沒有聲明wait屬性, 則通過add方法將模型添加到集合中      if(!options.wait)        coll.add(model, options);      // success存儲保存到服務器成功之后的自定義回調函數(通過options.success聲明)      var success = options.success;      // 監聽模型數據保存成功后的回調函數      options.success = function(nextModel, resp, xhr) {        // 如果聲明了wait屬性, 則在只有在服務器保存成功后才會將模型添加到集合中        if(options.wait)          coll.add(nextModel, options);        // 如果聲明了自定義成功回調, 則執行自定義函數, 否則將默認觸發模型的sync事件        if(success) {          success(nextModel, resp);        } else {          nextModel.trigger('sync', model, resp, options);        }      };      // 調用模型的save方法, 將模型數據保存到服務器      model.save(null, options);      return model;    },    // 數據解析方法, 用于將服務器數據解析為模型和集合可用的結構化數據    // 默認將返回resp本身, 這需要與服務器定義Backbone支持的數據格式, 如果需要自定義數據格式, 可以重載parse方法    parse : function(resp, xhr) {      return resp;    },    // chain用于構建集合數據的鏈式操作, 它將集合中的數據轉換為一個Underscore對象, 并使用Underscore的chain方法轉換為鏈式結構    // 關于chain方法的轉換方式, 可參考Underscore中chain方法的注釋    chain : function() {      return _(this.models).chain();    },    // 刪除所有集合元素并重置集合中的數據狀態    _reset : function(options) {      // 刪除集合元素      this.length = 0;      this.models = [];      // 重置集合狀態      this._byId = {};      this._byCid = {};    },    // 將模型添加到集合中之前的一些準備工作    // 包括將數據實例化為一個模型對象, 和將集合引用到模型的collection屬性    _prepareModel : function(model, options) {      options || ( options = {});      // 檢查model是否是一個模型對象(即Model類的實例)      if(!( model instanceof Model)) {        // 傳入的model是模型數據對象, 而并非模型對象        // 將數據作為參數傳遞給Model, 以創建一個新的模型對象        var attrs = model;        // 設置模型引用的集合        options.collection = this;        // 將數據轉化為模型        model = new this.model(attrs, options);        // 對模型中的數據進行驗證        if(!model._validate(model.attributes, options))          model = false;      } else if(!model.collection) {        // 如果傳入的是一個模型對象但沒有建立與集合的引用, 則設置模型的collection屬性為當前集合        model.collection = this;      }      return model;    },    // 解綁某個模型與集合的關系, 包括對集合的引用和事件監聽    // 一般在調用remove方法刪除模型或調用reset方法重置狀態時自動調用    _removeReference : function(model) {      // 如果模型引用了當前集合, 則移除該引用(必須確保所有對模型的引用已經解除, 否則模型可能無法從內存中釋放)      if(this == model.collection) {        delete model.collection;      }      // 取消集合中監聽的所有模型事件      model.off('all', this._onModelEvent, this);    },    // 在向集合中添加模型時被自動調用    // 用于監聽集合中模型的事件, 當模型在觸發事件(add, remove, destroy, change事件)時集合進行相關處理    _onModelEvent : function(event, model, collection, options) {      // 添加和移除模型的事件, 必須確保模型所屬的集合為當前集合對象      if((event == 'add' || event == 'remove') && collection != this)        return;      // 模型觸發銷毀事件時, 從集合中移除      if(event == 'destroy') {        this.remove(model, options);      }      // 當模型的id被修改時, 集合修改_byId中存儲對模型的引用, 保持與模型id的同步, 便于使用get()方法獲取模型對象      if(model && event === 'change:' + model.idAttribute) {        // 獲取模型在改變之前的id, 并根據此id從集合的_byId列表中移除        delete this._byId[model.previous(model.idAttribute)];        // 以模型新的id作為key, 在_byId列表中存放對模型的引用        this._byId[model.id] = model;      }      // 在集合中觸發模型對應的事件, 無論模型觸發任何事件, 集合都會觸發對應的事件      // (例如當模型被添加到集合中時, 會觸發模型的"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 = /[-[/]{}()+?.,//^$|#/s]/g;   // 向Router類的原型對象中擴展屬性和方法  _.extend(Router.prototype, Events, {     // 自定義初始化方法, 在路由器Router實例化后被自動調用    initialize : function() {    },    // 將一個路由規則綁定給一個監聽事件, 當URL片段匹配該規則時, 會自動調用觸發該事件    route : function(route, name, callback) {      // 創建history實例, Backbone.history是一個單例對象, 只在第一次創建路由器對象時被實例化      Backbone.history || (Backbone.history = new History);      // 檢查route規則名稱是否為一個字符串(當手動調用route方法創建路由規則時, 允許傳遞一個正則表達式或字符串作為規則)      // 在構造Router實例時傳入options.routes中的規則, 都應該是一個字符串(因為在_bindRoutes方法中將routes配置中的key作為路由規則)      // 如果傳入的是字符串類型的路由規則, 通過_routeToRegExp方法將其轉換為一個正則表達式, 用于匹配URL片段      if(!_.isRegExp(route))        route = this._routeToRegExp(route);      // 如果沒有設置callback(事件方法), 則根據name從當前Router實例中獲取與name同名的方法      // 這是因為在手動調用route方法時可能不會傳遞callback方法, 但必須傳遞name事件名稱, 并在Router實例中已經定義了該方法      if(!callback)        callback = this[name];      // 調用history實例的route方法, 該方法會將轉換后的正則表達式規則, 和監聽事件方法綁定到history.handlers列表中, 以便history進行路由和控制      // 當history實例匹配到對應的路由規則而調用該事件時, 會將URL片段作為字符串(即fragment參數)傳遞給該事件方法      // 這里并沒有直接將監聽事件傳遞給history的route方法, 而是使用bind方法封裝了另一個函數, 該函數的執行上下文為當前Router對象      Backbone.history.route(route, _.bind(function(fragment) {        // 調用_extractParameters方法獲取匹配到的規則中的參數        var args = this._extractParameters(route, fragment);        // 調用callback路由監聽事件, 并將參數傳遞給監聽事件        callback && callback.apply(this, args);        // 觸發route:name事件, name為調用route時傳遞的事件名稱        // 如果對當前Router實例使用on方法綁定了route:name事件, 則會收到該事件的觸發通知        this.trigger.apply(this, ['route:' + name].concat(args));        // 觸發history實例中綁定的route事件, 當路由器匹配到任何規則時, 均會觸發該事件        Backbone.history.trigger('route', this, name, args);        /**         * 事件綁定如:         * var router = new MyRouter();         * router.on('route:routename', function(param) {         *   // 綁定到Router實例中某個規則的事件, 當匹配到該規則時觸發         * });         * Backbone.history.on('route', function(router, name, args) {         *   // 綁定到history實例中的事件, 當匹配到任何規則時觸發         * });         * Backbone.history.start();         */      }, this));      return this;    },    // 通過調用history.navigate方法, 手動設置跳轉到URL    navigate : function(fragment, options) {      // 代理到history實例的navigate方法      Backbone.history.navigate(fragment, options);    },    // 解析當前實例定義的路由(this.routes)規則, 并調用route方法將每一個規則綁定到對應的方法    _bindRoutes : function() {      // 如果在創建對象時沒有設置routes規則, 則不進行解析和綁定      if(!this.routes)        return;      // routes變量以二維數組的形式存儲倒序排列的路由規則      // 如[['', 'homepage'], ['controller:name', 'toController']]      var routes = [];      // 遍歷routes配置      for(var route in this.routes) {        // 將路由規則放入一個新的數組, 按照[規則名稱, 綁定方法]組織        // 將該數組通過unshift方法放置到routes頂部, 實現倒序排列        // 這里將routes中的規則倒序排列, 在后面調用route方法時會再次調用unshift將順序倒過來, 以保證最終的順序是按照routes配置中定義的順序來執行的        // 倒換兩次順序后, 會重新恢復最初調用前的順序, 之所以這樣做, 是因為用戶可以手動調用route方法動態添加路由規則, 而手動添加的路由規則會被添加到列表的第一個, 因此要在route方法中使用unshift來插入規則        // 而構造Router實例時自動添加的規則, 為了保持定義順序, 因此在此處將定義的規則倒序排列        routes.unshift([route, this.routes[route]]);      }      // 循環完畢, 此時routes中存儲了倒序排列的路由規則       // 循環路由規則, 并依次調用route方法, 將規則名稱綁定到具體的事件函數      for(var i = 0, l = routes.length; i < l; i++) {        // 調用route方法, 并分別傳遞(規則名稱, 事件函數名, 事件函數對象)        this.route(routes[i][0], routes[i][1], this[routes[i][1]]);      }    },    // 將字符串形式的路由規則轉換為正則表達式對象    // (在route方法中檢查到字符串類型的路由規則后, 會自動調用該方法進行轉換)    _routeToRegExp : function(route) {      // 為字符串中特殊字符添加轉義符, 防止特殊字符在被轉換為正則表達式后變成元字符(這些特殊字符包括-[/]{}()+?.,//^$|#/s)      // 將字符串中以/"斜線"為分隔的動態路由規則轉換為([^//]+), 在正則中表示以/"斜線"開頭的多個字符      // 將字符串中的*"星號"動態路由規則轉換為(.*?), 在正則中表示0或多個任意字符(這里使用了非貪婪模式, 因此你可以使用例如這樣的組合路由規則: *list/:id, 將匹配 orderlist/123 , 同時會將"order"和"123"作為參數傳遞給事件方法 )      // 請注意namedParam和splatParam替換后的正則表達式都是用()括號將匹配的內容包含起來, 這是為了方便取出匹配的內容作為參數傳遞給事件方法      // 請注意namedParam和splatParam匹配的字符串 :str, *str中的str字符串是無意義的, 它們會在下面替換后被忽略, 但一般寫作和監聽事件方法的參數同名, 以便進行標識      route = route.replace(escapeRegExp, '//$&').replace(namedParam, '([^//]+)').replace(splatParam, '(.*?)');      // 將轉換后的字符串創建為正則表達式對象并返回      // 這個正則表達式將根據route字符串中的規則, 用于匹配URL片段      return new RegExp('^' + route + '$');    },    // 傳入一個路由規則(正則表達式)和URL片段(字符串)進行匹配, 并返回從匹配的字符串中獲取參數    /**     * 例如路由規則為 'teams/:type/:id', 對應的正則表達式會被轉換為/^teams/([^/]+)/([^/]+)$/ , (對路由規則轉換為正則表達式的過程可參考_routeToRegExp方法)     * URL片段為 'teams/35/1228'     * 則通過exec執行后的結果為 ["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 ? windowOverride.location : window.location;      // 將錨點(#)后的字符串提取出來并返回      var match = loc.href.match(/#(.*)$/);      // 如果沒有找到匹配的內容, 則返回空字符串      return match ? match[1] : '';    },    // 根據當前設置的路由方式, 處理并返回當前URL中的路由片段    getFragment : function(fragment, forcePushState) {      // fragment是通過getHash或從URL中已經提取的待處理路由片段(如 #/id/1288)      if(fragment == null) {// 如果沒有傳遞fragment, 則根據當前路由方式進行提取        if(this._hasPushState || forcePushState) {          // 使用了pushState方式進行路由          // fragment記錄當前域名后的URL路徑          fragment = window.location.pathname;          // search記錄當前頁面后的參數內容          var search = window.location.search;          // 將路徑和參數合并在一起, 作為待處理的路由片段          if(search)            fragment += search;        } else {          // 使用了hash方式進行路由          // 通過getHash方法獲取當前錨點(#)后的字符串作為路由片段          fragment = this.getHash();        }      }      // 根據配置項中設置的root參數, 則從路由片段取出root路徑之后的內容      if(!fragment.indexOf(this.options.root))        fragment = fragment.substr(this.options.root.length);      // 如果URL片段首字母為"#"或"/", 則去除該字符      // 返回處理之后的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="javascript:0" tabindex="-1" />').hide().appendTo('body')[0].contentWindow;        // 通過navigate將iframe設置到當前的URL片段, 這并不會真正加載到一個頁面, 因為fragment并非一個完整的URL        this.navigate(fragment);      }       // 開始監聽路由狀態變化      if(this._hasPushState) {        // 如果使用了pushState方式路由, 且瀏覽器支持該特性, 則將popstate事件監聽到checkUrl方法        $(window).bind('popstate', this.checkUrl);      } else if(this._wantsHashChange && ('onhashchange' in window) && !oldIE) {        // 如果使用Hash方式進行路由, 且瀏覽器支持onhashchange事件, 則將hashchange事件監聽到checkUrl方法        $(window).bind('hashchange', this.checkUrl);      } else if(this._wantsHashChange) {        // 對于低版本的瀏覽器, 通過setInterval方法心跳監聽checkUrl方法, interval屬性標識心跳頻率        this._checkUrlInterval = setInterval(this.checkUrl, this.interval);      }       // 記錄當前的URL片段      this.fragment = fragment;      // 驗證當前是否處于根路徑(即options.root中所配置的路徑)      var loc = window.location;      var atRoot = loc.pathname == this.options.root;       // 如果用戶通過pushState方式的URL訪問到當前地址, 但用戶此時所使用的瀏覽器并不支持pushState特性      // (這可能是某個用戶通過pushState方式訪問該應用, 然后將地址分享給其他用戶, 而其他用戶的瀏覽器并不支持該特性)      if(this._wantsHashChange && this._wantsPushState && !this._hasPushState && !atRoot) {        // 獲取當前pushState方式中的URL片段, 并通過Hash方式重新打開頁面        this.fragment = this.getFragment(null, true);        // 例如hashState方式的URL為 /root/topic/12001, 重新打開的Hash方式的URL則為 /root#topic/12001        window.location.replace(this.options.root + '#' + this.fragment);        return true;         // 如果用戶通過Hash方式的URL訪問到當前地址, 但調用Backbone.history.start方法時設置了pushState(希望通過pushState方式進行路由)        // 且用戶瀏覽器支持pushState特性, 則將當前URL替換為pushState方式(注意, 這里使用replaceState方式進行替換URL, 而頁面不會被刷新)        // 以下分支條件可理解為: 如果我們希望使用pushState方式進行路由, 且瀏覽器支持該特性, 同時用戶還使用了Hash方式打開當前頁面        // (這可能是某個用戶使用Hash方式瀏覽到一個URL, 并將URL分享給另一個瀏覽器支持pushState特性的用戶, 當該用戶訪問時會執行此分支)      } else if(this._wantsPushState && this._hasPushState && atRoot && loc.hash) {        // 獲取URL中的Hash片段, 并清除字符串首個"#"或"/"        this.fragment = this.getHash().replace(routeStripper, '');        // 使用replaceState方法將當前瀏覽器的URL替換為pushState支持的方式, 即: 協議//主機地址/URL路徑/Hash參數, 例如:        // 當用戶訪問Hash方式的URL為 /root/#topic/12001, 將被替換為 /root/topic/12001        // 注:        // pushState和replaceState方法的參數有3個, 分別是state, title, url        // -state: 用于存儲插入或修改的history實體信息        // -title: 用于設置瀏覽器標題(屬于保留參數, 目前瀏覽器還沒有實現該特性)        // -url: 設置history實體的URL地址(可以是絕對或相對路徑, 但無法設置跨域URL)        window.history.replaceState({}, document.title, loc.protocol + '//' + loc.host + this.options.root + this.fragment);      }       // 一般調用start方法時會自動調用loadUrl, 匹配當前URL片段對應的路由規則, 調用該規則的方法      // 如果設置了silent屬性為true, 則loadUrl方法不會被調用      // 這種情況一般出現在調用了stop方法重置history對象狀態后, 再次調用start方法啟動(實際上此時并非為頁面初始化, 因此會設置silent屬性)      if(!this.options.silent) {        return this.loadUrl();      }    },    // 停止history對路由的監控, 并將狀態恢復為未監聽狀態    // 調用stop方法之后, 可重新調用start方法開始監聽, stop方法一般用戶在調用start方法之后, 需要重新設置start方法的參數, 或用于單元測試    stop : function() {      // 解除對瀏覽器路由的onpopstate和onhashchange事件的監聽      $(window).unbind('popstate', this.checkUrl).unbind('hashchange', this.checkUrl);      // 停止對于低版本的IE瀏覽器的心跳監控      clearInterval(this._checkUrlInterval);      // 恢復started狀態, 便于下次重新調用start方法      History.started = false;    },    // 向handlers中綁定一個路由規則(參數route, 類型為正則表達式)與事件(參數callback)的映射關系(該方法由Router的實例自動調用)    route : function(route, callback) {      // 將route和callback插入到handlers列表的第一個位置      // 這是為了確保最后調用route時傳入的規則被優先進行匹配      this.handlers.unshift({        // 路由規則(正則)        route : route,        // 匹配規則時執行的方法        callback : callback      });    },    // 檢查當前的URL相對上一次的狀態是否發生了變化    // 如果發生變化, 則記錄新的URL狀態, 并調用loadUrl方法觸發新URL與匹配路由規則的方法    // 該方法在onpopstate和onhashchange事件被觸發后自動調用, 或者在低版本的IE瀏覽器中由setInterval心跳定時調用    checkUrl : function(e) {      // 獲取當前的URL片段      var current = this.getFragment();      // 對低版本的IE瀏覽器, 將從iframe中獲取最新的URL片段并賦給current變量      if(current == this.fragment && this.iframe)        current = this.getFragment(this.getHash(this.iframe));      // 如果當前URL與上一次的狀態沒有發生任何變化, 則停止執行      if(current == this.fragment)        return false;      // 執行到這里, URL已經發生改變, 調用navigate方法將URL設置為當前URL      // 這里在自動調用navigate方法時, 并沒有傳遞options參數, 因此不會觸發navigate方法中的loadUrl方法      if(this.iframe)        this.navigate(current);      // 調用loadUrl方法, 檢查匹配的規則, 并執行規則綁定的方法      // 如果調用this.loadUrl方法沒有成功, 則試圖在調用loadUrl方法時, 將重新獲取的當前Hash傳遞給該方法      this.loadUrl() || this.loadUrl(this.getHash());    },    // 根據當前URL, 與handler路由列表中的規則進行匹配    // 如果URL符合某一個規則, 則執行這個規則所對應的方法, 函數將返回true    // 如果沒有找到合適的規則, 將返回false    // loadUrl方法一般在頁面初始化時調用start方法會被自動調用(除非設置了silent參數為true)    // - 或當用戶改變URL后, 由checkUrl監聽到URL發生變化時被調用    // - 或當調用navigate方法手動導航到某個URL時被調用    loadUrl : function(fragmentOverride) {      // 獲取當前URL片段      var fragment = this.fragment = this.getFragment(fragmentOverride);      // 調用Undersocre的any方法, 將URL片段與handlers中的所有規則依次進行匹配      var matched = _.any(this.handlers, function(handler) {        // 如果handlers中的規則與當前URL片段匹配, 則執行該歸額對應的方法, 并返回true        if(handler.route.test(fragment)) {          handler.callback(fragment);          return true;        }      });      // matched是any方法的返回值, 如果匹配到規則則返回true, 沒有匹配到返回false      return matched;    },    // 導航到指定的URL    // 如果在options中設置了trigger, 將觸發導航的URL與對應路由規則的事件    // 如果在options中設置了replace, 將使用需要導航的URL替換當前的URL在history中的位置    navigate : function(fragment, options) {      // 如果沒有調用start方法, 或已經調用stop方法, 則無法導航      if(!History.started)        return false;      // 如果options參數不是一個對象, 而是true值, 則默認trigger配置項為true(即觸發導航的URL與對應路由規則的事件)      if(!options || options === true)        options = {          trigger : options        };      // 將傳遞的fragment(URL片段)去掉首字符的"#"或"/"      var frag = (fragment || '').replace(routeStripper, '');      // 如果當前URL與需要導航的URL沒有變化, 則不繼續執行      if(this.fragment == frag)        return;       // 如果當前支持并使用了pushState方式進行導航      if(this._hasPushState) {        // 構造一個完整的URL, 如果當前URL片段中沒有包含根路徑, 則使用根路徑連接URL片段        if(frag.indexOf(this.options.root) != 0)          frag = this.options.root + frag;        // 設置新的URL        this.fragment = frag;        // 如果在options選項中設置了replace屬性, 則將新的URL替換到history中的當前URL, 否則默認將新的URL追加到history中        window.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, frag);         // 如果使用hash方式進行導航      } else if(this._wantsHashChange) {        // 設置新的hash        this.fragment = frag;        // 調用_updateHash方法更新當前URL為新的hash, 并將options中的replace配置傳遞給_updateHash方法(在該方法中實現替換或追加新的hash)        this._updateHash(window.location, frag, options.replace);        // 對于低版本的IE瀏覽器, 當Hash發生變化時, 更新iframe URL中的Hash        if(this.iframe && (frag != this.getFragment(this.getHash(this.iframe)))) {          // 如果使用了replace參數替換當前URL, 則直接將iframe替換為新的文檔          // 調用document.open打開一個新的文檔, 以擦除當前文檔中的內容(這里調用close方法是為了關閉文檔的狀態)          // open和close方法之間沒有使用write或writeln方法輸出內容, 因此這是一個空文檔          if(!options.replace)            this.iframe.document.open().close();          // 調用_updateHash方法更新iframe中的URL          this._updateHash(this.iframe.location, frag, options.replace);        }       } else {        // 如果在調用start方法時, 手動設置hashChange參數為true, 不希望使用pushState和hash方式導航        // 則直接將頁面跳轉到新的URL        window.location.assign(this.options.root + fragment);      }      // 如果在options配置項中設置了trigger屬性, 則調用loadUrl方法查找路由規則, 并執行規則對應的事件      // 在URL發生變化時, 通過checkUrl方法監聽到的狀態, 會在checkUrl方法中自動調用loadUrl方法      // 在手動調用navigate方法時, 如果需要觸發路由事件, 則需要傳遞trigger參數      if(options.trigger)        this.loadUrl(fragment);    },    // 更新或設置當前URL中的Has串, _updateHash方法在使用hash方式導航時被自動調用(navigate方法中)    // location是需要更新hash的window.location對象    // fragment是需要更新的hash串    // 如果需要將新的hash替換到當前URL, 可以設置replace為true    _updateHash : function(location, fragment, replace) {      // 如果設置了replace為true, 則使用location.replace方法替換當前的URL      // 使用replace方法替換URL后, 新的URL將占有原有URL在history歷史中的位置      if(replace) {        // 將當前URL與hash組合為一個完整的URL并替換        location.replace(location.toString().replace(/(javascript:|#).*$/, '') + '#' + fragment);      } else {        // 沒有使用替換方式, 直接設置location.hash為新的hash串        location.hash = fragment;      }    }  });   // Backbone.View 視圖相關  // -------------   // 視圖類用于創建與數據低耦合的界面控制對象, 通過將視圖的渲染方法綁定到數據模型的change事件, 當數據發生變化時會通知視圖進行渲染  // 視圖對象中的el用于存儲當前視圖所需要操作的DOM最父層元素, 這主要是為了提高元素的查找和操作效率, 其優點包括:  // - 查找或操作元素時, 將操作的范圍限定在el元素內, 不需要再整個文檔樹中搜索  // - 在為元素綁定事件時, 可以方便地將事件綁定到el元素(默認也會綁定到el元素)或者是其子元素  // - 在設計模式中, 將一個視圖相關的元素, 事件, 和邏輯限定在該視圖的范圍中, 降低視圖與視圖間的耦合(至少在邏輯上是這樣)  var View = Backbone.View = function(options) {    // 為每一個視圖對象創建一個唯一標識, 前綴為"view"    this.cid = _.uniqueId('view');    // 設置初始化配置    this._configure(options || {});    // 設置或創建視圖中的元素    this._ensureElement();    // 調用自定義的初始化方法    this.initialize.apply(this, arguments);    // 解析options中設置的events事件列表, 并將事件綁定到視圖中的元素    this.delegateEvents();  };  // 定義用于解析events參數中事件名稱和元素的正則  var delegateEventSplitter = /^(/S+)/s*(.*)$/;   // viewOptions列表記錄一些列屬性名, 在構造視圖對象時, 如果傳遞的配置項中包含這些名稱, 則將屬性復制到對象本身  var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName'];   // 向視圖類的原型對象中添加一些方法  _.extend(View.prototype, Events, {     // 如果在創建視圖對象時, 沒有設置指定的el元素, 則會通過make方法創建一個元素, tagName為創建元素的默認標簽    // 也可以通過在options中自定義tagName來覆蓋默認的"div"標簽    tagName : 'div',     // 每個視圖中都具有一個$選擇器方法, 該方法與jQuery或Zepto類似, 通過傳遞一個表達式來獲取元素    // 但該方法只會在視圖對象的$el元素范圍內進行查找, 因此會提高匹配效率    $ : function(selector) {      return this.$el.find(selector);    },    // 初始化方法, 在對象被實例化后自動調用    initialize : function() {    },    // render方法與initialize方法類似, 默認沒有實現任何邏輯    // 一般會重載該方法, 以實現對視圖中元素的渲染    render : function() {      // 返回當前視圖對象, 以支持方法的鏈式操作      // 因此如果重載了該方法, 建議在方法最后也返回視圖對象(this)      return this;    },    // 移除當前視圖的$el元素    remove : function() {      // 通過調用jQuery或Zepto的remove方法, 因此在第三方庫中會同時移除該元素綁定的所有事件和數據      this.$el.remove();      return this;    },    // 根據傳入的標簽名稱, 屬性和內容, 創建并返回一個DOM元素    // 該方法用于在內部創建this.el時自動調用    make : function(tagName, attributes, content) {      // 根據tagName創建元素      var el = document.createElement(tagName);      // 設置元素屬性      if(attributes)        $(el).attr(attributes);      // 設置元素內容      if(content)        $(el).html(content);      // 返回元素      return el;    },    // 為視圖對象設置標準的$el及el屬性, 該方法在對象創建時被自動調用    // $el是通過jQuery或Zepto創建的對象, el是標準的DOM對象    setElement : function(element, delegate) {      // 如果已經存在了$el屬性(可能是手動調用了setElement方法切換視圖的元素), 則取消之前對$el綁定的events事件(詳細參考undelegateEvents方法)      if(this.$el)        this.undelegateEvents();      // 將元素創建為jQuery或Zepto對象, 并存放在$el屬性中      this.$el = ( element instanceof $) ? element : $(element);      // this.el存放標準的DOM對象      this.el = this.$el[0];      // 如果設置了delegate參數, 則為元素綁定視圖中events參數設置的事件      // 在視圖類的構造函數中, 已經調用了delegateEvents方法進行綁定, 因此在初始化的_ensureElement方法中調用setElement方法時沒有傳遞delegate參數      // 在手動調用setElemen方法設置視圖元素時, 允許傳遞delegate綁定事件      if(delegate !== false)        this.delegateEvents();      return this;    },    // 為視圖元素綁定事件    // events參數配置了需要綁定事件的集合, 格式如('事件名稱 元素選擇表達式' : '事件方法名稱/或事件函數'):    // {    //   'click #title': 'edit',    //   'click .save': 'save'    //   'click span': function() {}    // }    // 該方法在視圖對象初始化時會被自動調用, 并將對象中的events屬性作為events參數(事件集合)    delegateEvents : function(events) {      // 如果沒有手動傳遞events參數, 則從視圖對象獲取events屬性作為事件集合      if(!(events || ( events = getValue(this, 'events'))))        return;      // 取消當前已經綁定過的events事件      this.undelegateEvents();      // 遍歷需要綁定的事件列表      for(var key in events) {        // 獲取需要綁定的方法(允許是方法名稱或函數)        var method = events[key];        // 如果是方法名稱, 則從對象中獲取該函數對象, 因此該方法名稱必須是視圖對象中已定義的方法        if(!_.isFunction(method))          method = this[events[key]];        // 對無效的方法拋出一個錯誤        if(!method)          throw new Error('Method "' + events[key] + '" does not exist');        // 解析事件表達式(key), 從表達式中解析出事件的名字和需要操作的元素        // 例如 'click #title'將被解析為 'click' 和 '#title' 兩部分, 均存放在match數組中        var match = key.match(delegateEventSplitter);        // eventName為解析后的事件名稱        // selector為解析后的事件元素選擇器表達式        var eventName = match[1], selector = match[2];        // bind方法是Underscore中用于綁定函數上下文的方法        // 這里將method事件方法的上下文綁定到當前視圖對象, 因此在事件被觸發后, 事件方法中的this始終指向視圖對象本身        method = _.bind(method, this);        // 設置事件名稱, 在事件名稱后追加標識, 用于傳遞給jQuery或Zepto的事件綁定方法        eventName += '.delegateEvents' + this.cid;        // 通過jQuery或Zepto綁定事件        if(selector === '') {          // 如果沒有設置子元素選擇器, 則通過bind方法將事件和方法綁定到當前$el元素本身          this.$el.bind(eventName, method);        } else {          // 如果當前設置了子元素選擇器表達式, 則通過delegate方式綁定          // 該方法將查找當前$el元素下的子元素, 并將于selector表達式匹配的元素進行事件綁定          // 如果該選擇器的元素不屬于當前$el的子元素, 則事件綁定無效          this.$el.delegate(selector, eventName, method);        }      }    },    // 取消視圖中當前元素綁定的events事件, 該方法一般不會被使用    // 除非調用delegateEvents方法重新為視圖中的元素綁定事件, 在重新綁定之前會清除當前的事件    // 或通過setElement方法重新設置試圖的el元素, 也會清除當前元素的事件    undelegateEvents : function() {      this.$el.unbind('.delegateEvents' + this.cid);    },    // 在實例化視圖對象時設置初始配置    // 將傳遞的配置覆蓋到對象的options中    // 將配置中與viewOptions列表相同的配置復制到對象本身, 作為對象的屬性    _configure : function(options) {      // 如果對象本身設置了默認配置, 則使用傳遞的配置進行合并      if(this.options)        options = _.extend({}, this.options, options);      // 遍歷viewOptions列表      for(var i = 0, l = viewOptions.length; i < l; i++) {        // attr依次為viewOptions中的屬性名        var attr = viewOptions[i];        // 將options配置中與viewOptions相同的配置復制到對象本身, 作為對象的屬性        if(options[attr])          this[attr] = options[attr];      }      // 設置對象的options配置      this.options = options;    },    // 每一個視圖對象都應該有一個el元素, 作為渲染的元素    // 在構造視圖時, 可以設置對象的el屬性來指定一個元素    // 如果設置的el是一個字符串或DOM對象, 則通過$方法將其創建為一個jQuery或Zepto對象    // 如果沒有設置el屬性, 則根據傳遞的tagName, id和className, 調用mak方法創建一個元素    // (新創建的元素不會被添加到文檔樹中, 而始終存儲在內存, 當處理完畢需要渲染到頁面時, 一般會在重寫的render方法, 或自定義方法中, 訪問this.el將其追加到文檔)    // (如果我們需要向頁面添加一個目前還沒有的元素, 并且需要為其添加一些子元素, 屬性, 樣式或事件時, 可以通過該方式先將元素創建到內存, 在完成所有操作之后再手動渲染到文檔, 可以提高渲染效率)    _ensureElement : function() {      // 如果沒有設置el屬性, 則創建默認元素      if(!this.el) {        // 從對象獲取attributes屬性, 作為新創建元素的默認屬性列表        var attrs = getValue(this, 'attributes') || {};        // 設置新元素的id        if(this.id)          attrs.id = this.id;        // 設置新元素的class        if(this.className)          attrs['class'] = this.className;        // 通過make方法創建元素, 并調用setElement方法將元素設置為視圖所使用的標準元素        this.setElement(this.make(this.tagName, attrs), false);      } else {        // 如果設置了el屬性, 則直接調用setElement方法將el元素設置為視圖的標準元素        this.setElement(this.el, false);      }    }  });   // 實現對象繼承的函數, 該函數內部使用inherits實現繼承, 請參考inherits函數  var extend = function(protoProps, classProps) {    // child存儲已經實現繼承自當前類的子類(Function)    // protoProps設置子類原型鏈中的屬性    // classProps設置子類的靜態屬性    var child = inherits(this, protoProps, classProps);    // 將extend函數添加到子類, 因此調用子類的extend方法便可實現對子類的繼承    child.extend = this.extend;    // 返回實現繼承的子類    return child;  };  // 為Model, Collection, Router和View類實現繼承機制  Model.extend = Collection.extend = Router.extend = View.extend = extend;   // Backbone.sync 與服務器異步交互相關  // -------------   // 定義Backbone中與服務器交互方法和請求type的對應關系  var methodMap = {    'create' : 'POST',    'update' : 'PUT',    'delete' : 'DELETE',    'read' : 'GET'  };   // sync用于在Backbone中操作數據時, 向服務器發送請求同步數據狀態, 以建立與服務器之間的無縫連接  // sync發送默認通過第三方庫(jQuery, Zepto等) $.ajax方法發送請求, 因此如果要調用狀態同步相關的方法, 需要第三方庫支持  // Backbone默認定義了一套與服務器交互的數據格式(JSON)和結構, 服務器響應的數據應該遵循該約定  // 如果數據不需要保存在服務器, 或與服務器交互方法, 數據格式結構與約定不一致, 可以通過重載sync方法實現  // @param {String} method 在Backbone中執行的CRUD操作名稱  // @param {Model Obejct} model 需要與服務器同步狀態的模型對象  // @param {Object} options  Backbone.sync = function(method, model, options) {    // 根據CRUD方法名定義與服務器交互的方法(POST, GET, PUT, DELETE)    var type = methodMap[method];     // options默認為一個空對象    options || ( options = {});     // params將作為請求參數對象傳遞給第三方庫的$.ajax方法    var params = {      // 請求類型      type : type,      // 數據格式默認為json      dataType : 'json'    };     // 如果在發送請求時沒有在options中設置url地址, 將會通過模型對象的url屬性或方法來獲取url    // 模型所獲取url的方式可參考模型的url方法    if(!options.url) {      // 獲取請求地址失敗時會調用urlError方法拋出一個錯誤      params.url = getValue(model, 'url') || urlError();    }     // 如果調用create和update方法, 且沒有在options中定義請求數據, 將序列化模型中的數據對象傳遞給服務器    if(!options.data && model && (method == 'create' || method == 'update')) {      // 定義請求的Content-Type頭, 默認為application/json      params.contentType = 'application/json';      // 序列化模型中的數據, 并作為請求數據傳遞給服務器      params.data = JSON.stringify(model.toJSON());    }     // 對于不支持application/json編碼的瀏覽器, 可以通過設置Backbone.emulateJSON參數為true實現兼容    if(Backbone.emulateJSON) {      // 不支持Backbone.emulateJSON編碼的瀏覽器, 將類型設置為application/x-www-form-urlencoded      params.contentType = 'application/x-www-form-urlencoded';      // 將需要同步的數據存放在key為"model"參數中發送到服務器      params.data = params.data ? {        model : params.data      } : {};    }     // 對于不支持REST方式的瀏覽器, 可以設置Backbone.emulateHTTP參數為true, 以POST方式發送數據, 并在數據中加入_method參數標識操作名稱    // 同時也將發送X-HTTP-Method-Override頭信息    if(Backbone.emulateHTTP) {      // 如果操作類型為PUT或DELETE      if(type === 'PUT' || type === 'DELETE') {        // 將操作名稱存放到_method參數發送到服務器        if(Backbone.emulateJSON)          params.data._method = type;        // 實際以POST方式進行提交, 并發送X-HTTP-Method-Override頭信息        params.type = 'POST';        params.beforeSend = function(xhr) {          xhr.setRequestHeader('X-HTTP-Method-Override', type);        };      }    }     // 對非GET方式的請求, 將不對數據進行轉換, 因為傳遞的數據可能是一個JSON映射    if(params.type !== 'GET' && !Backbone.emulateJSON) {      // 通過設置processData為false來關閉數據轉換      // processData參數是$.ajax方法中的配置參數, 詳細信息可參考jQuery或Zepto相關文檔      params.processData = false;    }     // 通過第三方庫的$.ajax方法向服務器發送請求同步數據狀態    // 傳遞給$.ajax方法的參數使用extend方法將options對象中的參數覆蓋到了params對象, 因此在調用sync方法時設置了與params同名的options參數, 將以options為準    return $.ajax(_.extend(params, options));  };  // 包裝一個統一的模型錯誤處理方法, 會在模型與服務器交互發生錯誤時被調用  // onError是在調用與服務器的交互方法時(如fetch, destory等), options中指定的自定義錯誤處理函數  // originalModel是發生錯誤的模型或集合對象  Backbone.wrapError = function(onError, originalModel, options) {    return function(model, resp) {      resp = model === originalModel ? resp : model;       if(onError) {        // 如果設置了自定義錯誤處理方法, 則調用自定義方法        onError(originalModel, resp, options);      } else {        // 默認將觸發發生錯誤的模型或集合的error事件        originalModel.trigger('error', originalModel, resp, options);      }    };  };  // Helpers 定義一些供Backbone內部使用的幫助函數  // -------   // ctor是一個共享的空函數, 用于在調用inherits方法實現繼承時, 承載父類的原型鏈以便設置到子類原型中  var ctor = function() {  };  // 實現OOP繼承特性  // @param {Function} parent 被繼承的父類Function  // @param {Object} protoProps 擴展子類原型中的屬性(或方法)對象  // @param {Object} staticProps 擴展子類的靜態屬性(或方法)對象  var inherits = function(parent, protoProps, staticProps) {    var child;     // 如果在protoProps中指定了"constructor"屬性, 則"constructor"屬性被作為子類的構造函數    // 如果沒有指定構造子類構造函數, 則默認調用父類的構造函數    if(protoProps && protoProps.hasOwnProperty('constructor')) {      // 使用"constructor"屬性指定的子類構造函數      child = protoProps.constructor;    } else {      // 使用父類的構造函數      child = function() {        parent.apply(this, arguments);      };    }     // 將父類中的靜態屬性復制為子類靜態屬性    _.extend(child, parent);     // 將父類原型鏈設置到子類的原型對象中, 子類以此繼承父類原型鏈中的所有屬性    ctor.prototype = parent.prototype;    child.prototype = new ctor();     // 將protoProps對象中的屬性復制到子類的原型對象, 子類以此擁有protoProps中的屬性    if(protoProps)      _.extend(child.prototype, protoProps);     // 將staticProps對象中的屬性復制到子類的構造函數本身, 將staticProps中的屬性作為子類的靜態屬性    if(staticProps)      _.extend(child, staticProps);     // 在復制父類原型鏈到子類原型時, 子類原型鏈中的構造函數已經被覆蓋, 因此此處重新設置子類的構造函數    child.prototype.constructor = child;     // 如果子類設置了constructor屬性, 則子類構造函數為constructor指定的函數    // 如果需要在子類構造函數中調用父類構造函數, 則需要在子類構造函數中手動調用父類的構造函數    // 此處將子類的__super__屬性指向父類的構造函數, 方便在子類中調用: 子類.__super__.constructor.call(this);    child.__super__ = parent.prototype;     // 返回子類    return child;  };  // 獲取對象prop屬性的值, 如果prop屬性是一個函數, 則執行并返回該函數的返回值  var getValue = function(object, prop) {    // 如果object為空或object不存在prop屬性, 則返回null    if(!(object && object[prop]))      return null;    // 返回prop屬性值, 如果prop是一個函數, 則執行并返回該函數的返回值    return _.isFunction(object[prop]) ? object[prop]() : object[prop];  };  // 拋出一個Error異常, 在Backbone內部會頻繁執行, 因此獨立為一個公共函數  var urlError = function() {    throw new Error('A "url" property or function must be specified');  };}).call(this);
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
色婷婷综合成人| 国产成人综合亚洲| 亚洲最大福利视频| 亚洲激情 国产| 欧美极品美女视频网站在线观看免费| 日韩av电影国产| 色婷婷**av毛片一区| 91精品国产91久久久久福利| 欧美性猛交xxxxx水多| 亚洲美女av黄| 欧美色视频日本版| 九九久久综合网站| 亚洲欧美日韩久久久久久| 在线色欧美三级视频| 91高清视频在线免费观看| 久久6免费高清热精品| 欧美在线观看日本一区| 精品成人乱色一区二区| 国产精品久久77777| 精品久久久999| 欧美理论电影在线播放| 欧美日韩免费在线| 欧美高跟鞋交xxxxxhd| 欧美日韩中国免费专区在线看| 在线观看久久av| 欧美日韩免费区域视频在线观看| 九九久久综合网站| 成人午夜小视频| 日韩欧美福利视频| 国产精品欧美久久久| 国产精品∨欧美精品v日韩精品| 久久久午夜视频| 国产成人亚洲综合91精品| 国产视频自拍一区| 黑人欧美xxxx| 亚洲美女性生活视频| 成人福利网站在线观看| 欧美日韩亚洲91| 国产男女猛烈无遮挡91| 欧美极品少妇xxxxx| 亚洲欧美中文在线视频| 亚洲第一页中文字幕| 国产视频福利一区| 久久人人爽人人爽人人片亚洲| 国内精品视频一区| 亚洲男女自偷自拍图片另类| 欧美高清在线观看| 亚洲2020天天堂在线观看| 久久亚洲精品网站| 91夜夜揉人人捏人人添红杏| 精品视频在线播放| 日韩免费在线看| 国产精品久久久av| 国产精品免费视频xxxx| 久久久久国产精品免费网站| 96sao精品视频在线观看| 色妞色视频一区二区三区四区| 成人免费视频xnxx.com| 国产精品精品视频一区二区三区| 日韩精品中文字幕在线播放| 中文字幕亚洲无线码在线一区| 欧美激情在线有限公司| 91探花福利精品国产自产在线| 日韩av中文字幕在线| 亚洲最大福利视频网站| 日韩女优在线播放| 欧美精品免费看| 九色精品美女在线| 精品视频中文字幕| 国产欧美一区二区三区在线| 毛片精品免费在线观看| 久久国产精彩视频| 中文字幕欧美日韩| 国产99久久久欧美黑人| 欧美中文字幕在线观看| 日韩综合视频在线观看| 国产精品专区h在线观看| 精品国产依人香蕉在线精品| 国产suv精品一区二区三区88区| 欧美高跟鞋交xxxxxhd| 欧美高清不卡在线| 久久99精品国产99久久6尤物| 欧美壮男野外gaytube| 国内精品中文字幕| 国产精品美女www爽爽爽视频| 久久精品国产v日韩v亚洲| 久久精品影视伊人网| 亚洲一区二区三区四区视频| 川上优av一区二区线观看| 国产精品高潮在线| 亚洲第一区第二区| 亚洲尤物视频网| 日韩中文在线不卡| 91亚洲精华国产精华| 国产精品视频免费在线观看| 国产欧美日韩高清| 日韩av在线网| 欧美一区二区三区免费视| 亚洲国产成人久久综合一区| 欧美国产精品人人做人人爱| 欧美国产日韩一区| 亚洲视频在线视频| 亚洲激情久久久| 日韩中文字幕亚洲| 欧美成年人在线观看| 日韩av在线网| 亚洲三级黄色在线观看| 亚洲va欧美va国产综合久久| 欧美亚洲午夜视频在线观看| 久久国产精品99国产精| zzjj国产精品一区二区| 亚洲男人7777| 91精品在线观看视频| 韩国日本不卡在线| 日韩在线国产精品| 美女撒尿一区二区三区| 欧洲精品毛片网站| 一区二区成人av| 久久精品亚洲94久久精品| 亚洲嫩模很污视频| 国产亚洲精品久久久久久777| 日韩最新av在线| 一区二区三区国产视频| 亚洲第一页自拍| 精品日韩视频在线观看| 中文字幕一精品亚洲无线一区| 亚洲精品综合久久中文字幕| 国产精品高潮视频| 国产日韩在线一区| 亚洲97在线观看| 亚洲人成人99网站| 色悠悠久久久久| 最近2019中文字幕mv免费看| www.日韩系列| 久久在线免费视频| 亚洲天堂网在线观看| 亚洲精品wwwww| 日韩欧美大尺度| www国产亚洲精品久久网站| 亚洲第一页在线| 97在线看福利| 日韩在线观看免费| 韩国三级电影久久久久久| 欧美体内谢she精2性欧美| 欧美裸体xxxx极品少妇| 少妇高潮 亚洲精品| 日本不卡高字幕在线2019| 色www亚洲国产张柏芝| 久久久久北条麻妃免费看| 美女久久久久久久久久久| 一区二区国产精品视频| 国产精品羞羞答答| 欧美日韩免费在线观看| 国产精品成人免费电影| 亚洲香蕉av在线一区二区三区| 日韩一二三在线视频播| 日本精品中文字幕| 国产精品女主播视频| 欲色天天网综合久久| 亚洲图片在区色| 欧美一级成年大片在线观看| 国产美女搞久久| 欧美国产亚洲精品久久久8v| 欧美激情成人在线视频|