// 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