Backbone.js源碼解讀(轉載)

前言: 我的也翻譯過一遍,但是基礎知識不夠,因此理解的沒有很清楚 

// 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);
相關文章
相關標籤/搜索