// 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屬性進行依序關聯
/**資料格式如:
* {
* 尾部:{對象},
* 下一個: {
* 回呼:{函數},
* 上下文:{物件},
* 下一個: {
* 回呼:{函數},
* 上下文:{物件},
* 下一個:{物件}
* }
* }
* }*/
// 列表每一層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
/*** @格式選項
* {
* 解析:{布林值},
* 收藏:{收藏}
* }*/
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
有 : 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
登入後複製