(function(factory) {
Backbone.js 1.6.0
(c) 2010-2024 Jeremy Ashkenas and DocumentCloud
Backbone may be freely distributed under the MIT license.
For all details and documentation:
http://backbonejs.dev.org.tw
(function(factory) {
建立根物件,瀏覽器中的 window
(self
) 或伺服器上的 global
。我們使用 self
取代 window
以支援 WebWorker
。
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global;
適當地為環境設定 Backbone。從 AMD 開始。
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
即使在 AMD 案例中也匯出全域,以防此指令碼與仍可能預期全域 Backbone 的其他指令碼一起載入。
root.Backbone = factory(root, exports, _, $);
});
接著是 Node.js 或 CommonJS。jQuery 可能不需要作為模組。
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch (e) {}
factory(root, exports, _, $);
最後,作為瀏覽器全域。
} else {
root.Backbone = factory(root, {}, root._, root.jQuery || root.Zepto || root.ender || root.$);
}
})(function(root, Backbone, _, $) {
儲存 Backbone
變數的先前值,以便在使用 noConflict
時稍後可以還原。
var previousBackbone = root.Backbone;
建立對我們稍後會想要使用的共用陣列方法的本機參考。
var slice = Array.prototype.slice;
函式庫的目前版本。與 package.json
保持同步。
Backbone.VERSION = '1.6.0';
就 Backbone 的目的而言,jQuery、Zepto、Ender 或 My Library (開玩笑的) 擁有 $
變數。
Backbone.$ = $;
以 noConflict 模式執行 Backbone.js,將 Backbone
變數傳回給其先前擁有者。傳回對此 Backbone 物件的參考。
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
開啟 emulateHTTP
以支援舊式 HTTP 伺服器。設定此選項將透過 _method
參數偽造 "PATCH"
、"PUT"
和 "DELETE"
要求,並設定 X-Http-Method-Override
標頭。
Backbone.emulateHTTP = false;
開啟 emulateJSON
以支援無法處理直接 application/json
要求的舊式伺服器…這將改為將主體編碼為 application/x-www-form-urlencoded
,並將模型傳送至名為 model
的表單參數中。
Backbone.emulateJSON = false;
一個模組,可以混入 任何物件 中,以提供自訂事件頻道。您可以使用 on
將回呼繫結到事件,或使用 off
移除;trigger
事件會依序觸發所有回呼。
var object = {};
_.extend(object, Backbone.Events);
object.on('expand', function(){ alert('expanded'); });
object.trigger('expand');
var Events = Backbone.Events = {};
用於分割事件字串的正規表示式。
var eventSplitter = /\s+/;
一個私密全域變數,供偵聽器和被偵聽器共用。
var _listening;
反覆運算標準 event, callback
(以及精緻的多個空格分隔事件 "change blur", callback
和 jQuery 風格的事件對應 {event: callback}
)。
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
處理事件對應。
if (callback !== void 0 && 'context' in opts && opts.context === void 0) opts.context = callback;
for (names = _.keys(name); i < names.length ; i++) {
events = eventsApi(iteratee, events, names[i], name[names[i]], opts);
}
} else if (name && eventSplitter.test(name)) {
透過個別委派來處理空格分隔的事件名稱。
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
最後,標準事件。
events = iteratee(events, name, callback, opts);
}
return events;
};
將事件繫結到 callback
函式。傳遞 "all"
將會將回呼繫結到觸發的所有事件。
Events.on = function(name, callback, context) {
this._events = eventsApi(onApi, this._events || {}, name, callback, {
context: context,
ctx: this,
listening: _listening
});
if (_listening) {
var listeners = this._listeners || (this._listeners = {});
listeners[_listening.id] = _listening;
允許偵聽使用計數器,而不是追蹤回呼以進行函式庫互通。
_listening.interop = false;
}
return this;
};
on
的反轉控制版本。告訴 此 物件偵聽另一個物件中的事件…追蹤其偵聽的內容,以便稍後更容易取消繫結。
Events.listenTo = function(obj, name, callback) {
if (!obj) return this;
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = _listening = listeningTo[id];
此物件尚未偵聽 obj
上的任何其他事件。設定必要的參考以追蹤偵聽的回呼。
if (!listening) {
this._listenId || (this._listenId = _.uniqueId('l'));
listening = _listening = listeningTo[id] = new Listening(this, obj);
}
在 obj 上繫結回呼。
var error = tryCatchOn(obj, name, callback, this);
_listening = void 0;
if (error) throw error;
如果目標 obj 不是 Backbone.Events,請手動追蹤事件。
if (listening.interop) listening.on(name, callback);
return this;
};
將回呼新增到 events
物件的縮減 API。
var onApi = function(events, name, callback, options) {
if (callback) {
var handlers = events[name] || (events[name] = []);
var context = options.context, ctx = options.ctx, listening = options.listening;
if (listening) listening.count++;
handlers.push({callback: callback, context: context, ctx: context || ctx, listening: listening});
}
return events;
};
一個 try-catch 保護的 #on 函數,用於防止污染全域變數 _listening
。
var tryCatchOn = function(obj, name, callback, context) {
try {
obj.on(name, callback, context);
} catch (e) {
return e;
}
};
移除一個或多個回呼。如果 context
為 null,則移除所有具有該函數的回呼。如果 callback
為 null,則移除事件的所有回呼。如果 name
為 null,則移除所有事件的所有繫結回呼。
Events.off = function(name, callback, context) {
if (!this._events) return this;
this._events = eventsApi(offApi, this._events, name, callback, {
context: context,
listeners: this._listeners
});
return this;
};
告訴這個物件停止偵聽特定事件…或停止偵聽它目前偵聽的每個物件。
Events.stopListening = function(obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];
如果偵聽不存在,則此物件目前未偵聽 obj。提早中斷。
if (!listening) break;
listening.obj.off(name, callback, this);
if (listening.interop) listening.off(name, callback);
}
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
};
從 events
物件中移除回呼的簡化 API。
var offApi = function(events, name, callback, options) {
if (!events) return;
var context = options.context, listeners = options.listeners;
var i = 0, names;
刪除所有事件偵聽器和「中斷」事件。
if (!name && !context && !callback) {
for (names = _.keys(listeners); i < names.length; i++) {
listeners[names[i]].cleanup();
}
return;
}
names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
如果沒有儲存事件,則放棄。
if (!handlers) break;
尋找任何剩餘事件。
var remaining = [];
for (var j = 0; j < handlers.length; j++) {
var handler = handlers[j];
if (
callback && callback !== handler.callback &&
callback !== handler.callback._callback ||
context && context !== handler.context
) {
remaining.push(handler);
} else {
var listening = handler.listening;
if (listening) listening.off(name, callback);
}
}
如果有任何剩餘事件,則替換事件。否則,清理。
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
return events;
};
繫結事件以僅觸發一次。在第一次呼叫回呼後,將移除其偵聽器。如果使用以空格分隔的語法傳入多個事件,則處理常式將針對每個事件觸發一次,而不是針對所有事件的組合觸發一次。
Events.once = function(name, callback, context) {
將事件對應到 {event: once}
物件。
var events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
if (typeof name === 'string' && context == null) callback = void 0;
return this.on(events, callback, context);
};
once
的控制反轉版本。
Events.listenToOnce = function(obj, name, callback) {
將事件對應到 {event: once}
物件。
var events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
return this.listenTo(obj, events);
};
將事件回呼簡化為 {event: onceWrapper}
的對應。offer
在呼叫 onceWrapper
之後取消繫結。
var onceMap = function(map, name, callback, offer) {
if (callback) {
var once = map[name] = _.once(function() {
offer(name, once);
callback.apply(this, arguments);
});
once._callback = callback;
}
return map;
};
觸發一個或多個事件,觸發所有繫結的回呼。回呼傳遞的參數與 trigger
相同,除了事件名稱(除非您在 "all"
上偵聽,這將導致您的回呼接收事件的真實名稱作為第一個參數)。
Events.trigger = function(name) {
if (!this._events) return this;
var length = Math.max(0, arguments.length - 1);
var args = Array(length);
for (var i = 0; i < length; i++) args[i] = arguments[i + 1];
eventsApi(triggerApi, this._events, name, void 0, args);
return this;
};
處理觸發適當的事件回呼。
var triggerApi = function(objEvents, name, callback, args) {
if (objEvents) {
var events = objEvents[name];
var allEvents = objEvents.all;
if (events && allEvents) allEvents = allEvents.slice();
if (events) triggerEvents(events, args);
if (allEvents) triggerEvents(allEvents, [name].concat(args));
}
return objEvents;
};
一個難以置信但經過最佳化的內部調度函數,用於觸發事件。嘗試讓一般情況保持快速(大多數內部 Backbone 事件有 3 個參數)。
var triggerEvents = function(events, args) {
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
switch (args.length) {
case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
}
};
一個偵聽類別,在所有回呼都已關閉時追蹤和清理記憶體繫結。
var Listening = function(listener, obj) {
this.id = listener._listenId;
this.listener = listener;
this.obj = obj;
this.interop = true;
this.count = 0;
this._events = void 0;
};
Listening.prototype.on = Events.on;
關閉一個回呼(或多個)。如果偵聽者使用 Backbone.Events,則使用最佳化的計數器。否則,退回手動追蹤以支援事件程式庫互通。
Listening.prototype.off = function(name, callback) {
var cleanup;
if (this.interop) {
this._events = eventsApi(offApi, this._events, name, callback, {
context: void 0,
listeners: void 0
});
cleanup = !this._events;
} else {
this.count--;
cleanup = this.count === 0;
}
if (cleanup) this.cleanup();
};
清理偵聽器和偵聽物件之間的記憶體繫結。
Listening.prototype.cleanup = function() {
delete this.listener._listeningTo[this.obj._listenId];
if (!this.interop) delete this.obj._listeners[this.id];
};
向後相容性的別名。
Events.bind = Events.on;
Events.unbind = Events.off;
允許 Backbone
物件作為全域事件匯流排,供想要在方便的地方使用全域「pubsub」的人使用。
_.extend(Backbone, Events);
Backbone Models 是架構中的基本資料物件 - 通常代表伺服器上資料庫中表格中的列。一個離散的資料區塊和一堆有用的相關方法,用於對該資料執行運算和轉換。
使用指定的屬性建立一個新模型。將自動為您產生並指定一個客戶端 ID (cid
)。
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
this.preinitialize.apply(this, arguments);
this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {};
if (options.collection) this.collection = options.collection;
if (options.parse) attrs = this.parse(attrs, options) || {};
var defaults = _.result(this, 'defaults');
僅使用 _.defaults 即可正常運作,但額外的 _.extends 出於歷史原因而存在。請參閱 #3843。
attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
this.set(attrs, options);
this.changed = {};
this.initialize.apply(this, arguments);
};
將所有可繼承的方法附加到 Model 原型。
_.extend(Model.prototype, Events, {
當前值和前一個值不同的屬性雜湊。
changed: null,
上次驗證失敗期間傳回的值。
validationError: null,
JSON id
屬性的預設名稱為 "id"
。MongoDB 和 CouchDB 使用者可能想要將其設定為 "_id"
。
idAttribute: 'id',
前置詞用於建立用於在本地識別模型的客戶端 ID。如果您遇到模型 ID 的名稱衝突,則可能需要覆寫此內容。
cidPrefix: 'c',
preinitialize 預設為空函式。您可以使用函式或物件覆寫它。preinitialize 將在 Model 中執行任何實例化邏輯之前執行。
preinitialize: function(){},
Initialize 預設為空函式。使用您自己的初始化邏輯覆寫它。
initialize: function(){},
傳回模型 attributes
物件的副本。
toJSON: function(options) {
return _.clone(this.attributes);
},
預設代理 Backbone.sync
– 但如果您需要針對此特定模型自訂同步語意,請覆寫此內容。
sync: function() {
return Backbone.sync.apply(this, arguments);
},
取得屬性的值。
get: function(attr) {
return this.attributes[attr];
},
取得屬性的 HTML 轉譯值。
escape: function(attr) {
return _.escape(this.get(attr));
},
如果屬性包含非 null 或未定義的值,則傳回 true
。
has: function(attr) {
return this.get(attr) != null;
},
下底線 _.matches
方法的特殊案例代理。
matches: function(attrs) {
return !!_.iteratee(attrs, this)(this.attributes);
},
在物件上設定模型屬性雜湊,觸發 "change"
。這是模型的核心原始操作,更新資料並通知任何需要知道狀態變更的人。野獸的心臟。
set: function(key, val, options) {
if (key == null) return this;
處理 "key", value
和 {key: value}
型式的引數。
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
執行驗證。
if (!this._validate(attrs, options)) return false;
擷取屬性和選項。
var unset = options.unset;
var silent = options.silent;
var changes = [];
var changing = this._changing;
this._changing = true;
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
var current = this.attributes;
var changed = this.changed;
var prev = this._previousAttributes;
對於每個 set
屬性,更新或刪除當前值。
for (var attr in attrs) {
val = attrs[attr];
if (!_.isEqual(current[attr], val)) changes.push(attr);
if (!_.isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
}
更新 id
。
if (this.idAttribute in attrs) {
var prevId = this.id;
this.id = this.get(this.idAttribute);
this.trigger('changeId', this, prevId, options);
}
觸發所有相關屬性變更。
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
您可能會想知道為什麼這裡有一個 while
迴圈。變更可以在 "change"
事件中遞迴巢狀。
if (changing) return this;
if (!silent) {
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
},
從模型中移除屬性,觸發 "change"
。如果屬性不存在,則 unset
為空操作。
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
清除模型上的所有屬性,觸發 "change"
。
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
判斷模型是否自上次 "change"
事件後已變更。如果您指定屬性名稱,則判斷該屬性是否已變更。
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
傳回包含所有已變更屬性的物件,或在沒有變更屬性的情況下傳回 false。對於判斷檢視的哪些部分需要更新和/或哪些屬性需要持續到伺服器很有用。未設定的屬性將設定為未定義。您也可以傳遞屬性物件以與模型進行比對,判斷是否有變更。
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
var hasChanged;
for (var attr in diff) {
var val = diff[attr];
if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
hasChanged = true;
}
return hasChanged ? changed : false;
},
取得上一個屬性的值,記錄在最後一次觸發的 "change"
事件時間。
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
取得上一個 "change"
事件時間的模型所有屬性。
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
從伺服器擷取模型,將回應與模型的本機屬性合併。任何變更的屬性都會觸發「變更」事件。
fetch: function(options) {
options = _.extend({parse: true}, options);
var model = this;
var success = options.success;
options.success = function(resp) {
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (!model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
設定模型屬性的雜湊,並將模型同步到伺服器。如果伺服器傳回不同的屬性雜湊,模型的狀態將再次 set
。
save: function(key, val, options) {
處理 "key", value
和 {key: value}
型式的引數。
var attrs;
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options = _.extend({validate: true, parse: true}, options);
var wait = options.wait;
如果我們沒有等待且屬性存在,則儲存動作會像 set(attr).save(null, opts)
一樣,並進行驗證。否則,檢查模型在設定屬性(如果有)時是否有效。
if (attrs && !wait) {
if (!this.set(attrs, options)) return false;
} else if (!this._validate(attrs, options)) {
return false;
}
在伺服器端儲存成功後,客戶端(選擇性地)會更新為伺服器端狀態。
var model = this;
var success = options.success;
var attributes = this.attributes;
options.success = function(resp) {
確保在同步儲存期間屬性會復原。
model.attributes = attributes;
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
if (serverAttrs && !model.set(serverAttrs, options)) return false;
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
wrapError(this, options);
如果 {wait: true}
,則設定暫時屬性以正確找到新的 id。
if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
var method = this.isNew() ? 'create' : options.patch ? 'patch' : 'update';
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this.sync(method, this, options);
復原屬性。
this.attributes = attributes;
return xhr;
},
如果此模型已持續,則在伺服器上銷毀此模型。如果模型有集合,則樂觀地從集合中移除模型。如果傳遞 wait: true
,則在移除前等待伺服器回應。
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var wait = options.wait;
var destroy = function() {
model.stopListening();
model.trigger('destroy', model, model.collection, options);
};
options.success = function(resp) {
if (wait) destroy();
if (success) success.call(options.context, model, resp, options);
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
var xhr = false;
if (this.isNew()) {
_.defer(options.success);
} else {
wrapError(this, options);
xhr = this.sync('delete', this, options);
}
if (!wait) destroy();
return xhr;
},
模型在伺服器上表示的預設 URL – 如果您使用 Backbone 的 restful 方法,請覆寫此方法以變更將呼叫的終端點。
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
var id = this.get(this.idAttribute);
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
},
parse 會將回應轉換為要在模型上 set
的屬性雜湊。預設實作僅傳遞回應。
parse: function(resp, options) {
return resp;
},
建立一個與此模型屬性相同的模型。
clone: function() {
return new this.constructor(this.attributes);
},
如果模型從未儲存到伺服器且沒有 id,則模型是新的。
isNew: function() {
return !this.has(this.idAttribute);
},
檢查模型目前是否處於有效狀態。
isValid: function(options) {
return this._validate({}, _.extend({}, options, {validate: true}));
},
針對下一組完整的模型屬性執行驗證,如果一切順利,則傳回 true
。否則,觸發 "invalid"
事件。
_validate: function(attrs, options) {
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
var error = this.validationError = this.validate(attrs, options) || null;
if (!error) return true;
this.trigger('invalid', this, error, _.extend(options, {validationError: error}));
return false;
}
});
如果模型傾向於表示單一資料列,則 Backbone 集合更類似於資料表 … 或該資料表的小部分或頁面,或基於特定原因而屬於同一類別的資料列集合 – 此特定資料夾中的所有訊息、屬於此特定作者的所有文件,等等。集合會維護其模型的索引,依序和按 id
查詢。
建立一個新的 集合,可能包含特定類型的 模型
。如果指定 比較器
,則集合會在新增和移除模型時,維持模型的排序順序。
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
this.preinitialize.apply(this, arguments);
if (options.model) this.model = options.model;
if (options.comparator !== void 0) this.comparator = options.comparator;
this._reset();
this.initialize.apply(this, arguments);
if (models) this.reset(models, _.extend({silent: true}, options));
};
Collection#set
的預設選項。
var setOptions = {add: true, remove: true, merge: true};
var addOptions = {add: true, remove: false};
將 insert
拼接到 array
中的索引 at
。
var splice = function(array, insert, at) {
at = Math.min(Math.max(at, 0), array.length);
var tail = Array(array.length - at);
var length = insert.length;
var i;
for (i = 0; i < tail.length; i++) tail[i] = array[i + at];
for (i = 0; i < length; i++) array[i + at] = insert[i];
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
};
定義 Collection 的可繼承方法。
_.extend(Collection.prototype, Events, {
集合的預設模型僅為 Backbone.Model。在多數情況下,應覆寫此模型。
model: Model,
preinitialize 預設為空函式。您可以使用函式或物件覆寫它。preinitialize 會在 Collection 中執行任何實例化邏輯之前執行。
preinitialize: function(){},
Initialize 預設為空函式。使用您自己的初始化邏輯覆寫它。
initialize: function(){},
Collection 的 JSON 表示形式為模型屬性的陣列。
toJSON: function(options) {
return this.map(function(model) { return model.toJSON(options); });
},
預設代理 Backbone.sync
。
sync: function() {
return Backbone.sync.apply(this, arguments);
},
將模型或模型清單新增至集合。models
可以是 Backbone 模型或要轉換為模型的原始 JavaScript 物件,或兩者的任意組合。
add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
},
從集合中移除模型或模型清單。
remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();
var removed = this._removeModels(models, options);
if (!options.silent && removed.length) {
options.changes = {added: [], merged: [], removed: removed};
this.trigger('update', this, options);
}
return singular ? removed[0] : removed;
},
透過 set
設定新的模型清單來更新集合,視需要新增新模型、移除不再存在的模型,以及合併已存在於集合中的模型。類似於 Model#set,這是用於更新集合所包含資料的核心操作。
set: function(models, options) {
if (models == null) return;
options = _.extend({}, setOptions, options);
if (options.parse && !this._isModel(models)) {
models = this.parse(models, options) || [];
}
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();
var at = options.at;
if (at != null) at = +at;
if (at > this.length) at = this.length;
if (at < 0) at += this.length + 1;
var set = [];
var toAdd = [];
var toMerge = [];
var toRemove = [];
var modelMap = {};
var add = options.add;
var merge = options.merge;
var remove = options.remove;
var sort = false;
var sortable = this.comparator && at == null && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null;
將純粹物件轉換為模型參考,並防止加入無效模型。
var model, i;
for (i = 0; i < models.length; i++) {
model = models[i];
如果找到重複項,則防止它被加入,並可選擇將其合併到現有模型中。
var existing = this.get(model);
if (existing) {
if (merge && model !== existing) {
var attrs = this._isModel(model) ? model.attributes : model;
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
toMerge.push(existing);
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}
models[i] = existing;
如果這是新的有效模型,則將其推送到 toAdd
清單中。
} else if (add) {
model = models[i] = this._prepareModel(model, options);
if (model) {
toAdd.push(model);
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
}
移除過時的模型。
if (remove) {
for (i = 0; i < this.length; i++) {
model = this.models[i];
if (!modelMap[model.cid]) toRemove.push(model);
}
if (toRemove.length) this._removeModels(toRemove, options);
}
查看是否需要排序,更新 length
並將新模型插入。
var orderChanged = false;
var replace = !sortable && add && remove;
if (set.length && replace) {
orderChanged = this.length !== set.length || _.some(this.models, function(m, index) {
return m !== set[index];
});
this.models.length = 0;
splice(this.models, set, 0);
this.length = this.models.length;
} else if (toAdd.length) {
if (sortable) sort = true;
splice(this.models, toAdd, at == null ? this.length : at);
this.length = this.models.length;
}
在適當的情況下,靜默地對集合進行排序。
if (sort) this.sort({silent: true});
除非靜默,否則現在是觸發所有適當的 add/sort/update 事件的時候了。
if (!options.silent) {
for (i = 0; i < toAdd.length; i++) {
if (at != null) options.index = at + i;
model = toAdd[i];
model.trigger('add', model, this, options);
}
if (sort || orderChanged) this.trigger('sort', this, options);
if (toAdd.length || toRemove.length || toMerge.length) {
options.changes = {
added: toAdd,
removed: toRemove,
merged: toMerge
};
this.trigger('update', this, options);
}
}
傳回已新增(或已合併)的模型(或模型)。
return singular ? models[0] : models;
},
當您有比個別新增或移除更多的項目時,您可以使用新的模型清單重設整個集合,而不會觸發任何細部的 add
或 remove
事件。完成後觸發 reset
。適用於大量操作和最佳化。
reset: function(models, options) {
options = options ? _.clone(options) : {};
for (var i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options);
}
options.previousModels = this.models;
this._reset();
models = this.add(models, _.extend({silent: true}, options));
if (!options.silent) this.trigger('reset', this, options);
return models;
},
將模型新增至集合的尾端。
push: function(model, options) {
return this.add(model, _.extend({at: this.length}, options));
},
從集合的尾端移除模型。
pop: function(options) {
var model = this.at(this.length - 1);
return this.remove(model, options);
},
將模型新增至集合的開頭。
unshift: function(model, options) {
return this.add(model, _.extend({at: 0}, options));
},
從集合的開頭移除模型。
shift: function(options) {
var model = this.at(0);
return this.remove(model, options);
},
從集合中切片出模型的子陣列。
slice: function() {
return slice.apply(this.models, arguments);
},
透過 id、cid、具有 id 或 cid 屬性的模型物件,或透過 modelId 轉換的屬性物件,從集合中取得模型。
get: function(obj) {
if (obj == null) return void 0;
return this._byId[obj] ||
this._byId[this.modelId(this._isModel(obj) ? obj.attributes : obj, obj.idAttribute)] ||
obj.cid && this._byId[obj.cid];
},
如果模型在集合中,則傳回 true
。
has: function(obj) {
return this.get(obj) != null;
},
取得給定索引處的模型。
at: function(index) {
if (index < 0) index += this.length;
return this.models[index];
},
傳回具有相符屬性的模型。適用於 filter
的簡單案例。
where: function(attrs, first) {
return this[first ? 'find' : 'filter'](attrs);
},
傳回具有相符屬性的第一個模型。適用於 find
的簡單案例。
findWhere: function(attrs) {
return this.where(attrs, true);
},
強制集合重新對自身進行排序。在正常情況下,您不需要呼叫此函式,因為集合會在新增每個項目時維持排序順序。
sort: function(options) {
var comparator = this.comparator;
if (!comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
var length = comparator.length;
if (_.isFunction(comparator)) comparator = comparator.bind(this);
根據 comparator
類型執行排序。
if (length === 1 || _.isString(comparator)) {
this.models = this.sortBy(comparator);
} else {
this.models.sort(comparator);
}
if (!options.silent) this.trigger('sort', this, options);
return this;
},
從集合中的每個模型中提取一個屬性。
pluck: function(attr) {
return this.map(attr + '');
},
擷取此集合的預設模型組,並在它們抵達時重設集合。如果傳遞 reset: true
,則會透過 reset
方法傳遞回應資料,而不是透過 set
。
fetch: function(options) {
options = _.extend({parse: true}, options);
var success = options.success;
var collection = this;
options.success = function(resp) {
var method = options.reset ? 'reset' : 'set';
collection[method](resp, options);
if (success) success.call(options.context, collection, resp, options);
collection.trigger('sync', collection, resp, options);
};
wrapError(this, options);
return this.sync('read', this, options);
},
在此集合中建立模型的新執行個體。立即將模型新增到集合中,除非傳遞 wait: true
,否則我們會等到伺服器同意。
create: function(model, options) {
options = options ? _.clone(options) : {};
var wait = options.wait;
model = this._prepareModel(model, options);
if (!model) return false;
if (!wait) this.add(model, options);
var collection = this;
var success = options.success;
options.success = function(m, resp, callbackOpts) {
if (wait) {
m.off('error', collection._forwardPristineError, collection);
collection.add(m, callbackOpts);
}
if (success) success.call(callbackOpts.context, m, resp, callbackOpts);
};
如果 wait:true,我們的集合尚未偵聽任何模型事件,因此它不會轉送錯誤事件。在此特殊情況下,我們需要個別偵聽並僅處理一次事件。(我們不需要對同步事件執行此操作的原因在於上述的成功處理常式:我們會先新增模型,這會導致集合偵聽,然後呼叫觸發事件的回呼。)
if (wait) {
model.once('error', this._forwardPristineError, this);
}
model.save(null, options);
return model;
},
parse 會將回應轉換成要新增到集合的模型清單。預設實作只是傳遞它。
parse: function(resp, options) {
return resp;
},
建立一個新集合,其中包含與此集合相同的模型清單。
clone: function() {
return new this.constructor(this.models, {
model: this.model,
comparator: this.comparator
});
},
定義如何唯一識別集合中的模型。
modelId: function(attrs, idAttribute) {
return attrs[idAttribute || this.model.prototype.idAttribute || 'id'];
},
取得此集合中所有模型的迭代器。
values: function() {
return new CollectionIterator(this, ITERATOR_VALUES);
},
取得此集合中所有模型 ID 的迭代器。
keys: function() {
return new CollectionIterator(this, ITERATOR_KEYS);
},
取得此集合中所有 [ID、模型] 組合的迭代器。
entries: function() {
return new CollectionIterator(this, ITERATOR_KEYSVALUES);
},
重設所有內部狀態的私人方法。在集合首次初始化或重設時呼叫。
_reset: function() {
this.length = 0;
this.models = [];
this._byId = {};
},
準備要新增到此集合的屬性雜湊 (或其他模型)。
_prepareModel: function(attrs, options) {
if (this._isModel(attrs)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options ? _.clone(options) : {};
options.collection = this;
var model;
if (this.model.prototype) {
model = new this.model(attrs, options);
} else {
ES 類別方法沒有原型。
model = this.model(attrs, options);
}
if (!model.validationError) return model;
this.trigger('invalid', this, model.validationError, options);
return false;
},
移除和設定都會呼叫的內部方法。
_removeModels: function(models, options) {
var removed = [];
for (var i = 0; i < models.length; i++) {
var model = this.get(models[i]);
if (!model) continue;
var index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
在觸發「移除」事件之前移除參考,以防止無限迴圈。#3693
delete this._byId[model.cid];
var id = this.modelId(model.attributes, model.idAttribute);
if (id != null) delete this._byId[id];
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
removed.push(model);
this._removeReference(model, options);
}
if (models.length > 0 && !options.silent) delete options.index;
return removed;
},
用於檢查物件是否應視為模型以新增到集合的方法。
_isModel: function(model) {
return model instanceof Model;
},
建立模型與集合關聯的內部方法。
_addReference: function(model, options) {
this._byId[model.cid] = model;
var id = this.modelId(model.attributes, model.idAttribute);
if (id != null) this._byId[id] = model;
model.on('all', this._onModelEvent, this);
},
切斷模型與集合關聯的內部方法。
_removeReference: function(model, options) {
delete this._byId[model.cid];
var id = this.modelId(model.attributes, model.idAttribute);
if (id != null) delete this._byId[id];
if (this === model.collection) delete model.collection;
model.off('all', this._onModelEvent, this);
},
每次集合中的模型觸發事件時呼叫的內部方法。當模型變更 ID 時,集合需要更新其索引。所有其他事件僅透過代理傳遞。「新增」和「移除」事件源自其他集合時會被忽略。
_onModelEvent: function(event, model, collection, options) {
if (model) {
if ((event === 'add' || event === 'remove') && collection !== this) return;
if (event === 'destroy') this.remove(model, options);
if (event === 'changeId') {
var prevId = this.modelId(model.previousAttributes(), model.idAttribute);
var id = this.modelId(model.attributes, model.idAttribute);
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
}
}
this.trigger.apply(this, arguments);
},
create
中使用的內部回呼方法。它用作 _onModelEvent
方法的替身,該方法在 create
呼叫的 wait
期間尚未繫結。我們仍希望在 wait
期間結束時轉送任何 'error'
事件,因此需要自訂回呼。
_forwardPristineError: function(model, collection, options) {
如果模型在呼叫 create
之前已存在於集合中,則防止重複轉送。
if (this.has(model)) return;
this._onModelEvent('error', model, collection, options);
}
});
定義 @@iterator 方法會實作 JavaScript 的 Iterable 協定。在現代的 ES2015 瀏覽器中,這個值會在 Symbol.iterator 中找到。
/* global Symbol */
var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
if ($$iterator) {
Collection.prototype[$$iterator] = Collection.prototype.values;
}
CollectionIterator 實作 JavaScript 的 Iterator 協定,允許在現代瀏覽器中使用 for of
迴圈,以及 Backbone.Collection 與其他 JavaScript 函式和可對可迭代物件運作的第三方函式庫之間的互操作。
var CollectionIterator = function(collection, kind) {
this._collection = collection;
this._kind = kind;
this._index = 0;
};
此「枚舉」定義 CollectionIterator 可能發出的三種可能值,分別對應 Collection 上的 values()、keys() 和 entries() 方法。
var ITERATOR_VALUES = 1;
var ITERATOR_KEYS = 2;
var ITERATOR_KEYSVALUES = 3;
所有 Iterator 本身都應該是可迭代的。
if ($$iterator) {
CollectionIterator.prototype[$$iterator] = function() {
return this;
};
}
CollectionIterator.prototype.next = function() {
if (this._collection) {
僅在迭代的集合夠長時繼續迭代。
if (this._index < this._collection.length) {
var model = this._collection.at(this._index);
this._index++;
根據應該迭代的值類型建構一個值。
var value;
if (this._kind === ITERATOR_VALUES) {
value = model;
} else {
var id = this._collection.modelId(model.attributes, model.idAttribute);
if (this._kind === ITERATOR_KEYS) {
value = id;
} else { // ITERATOR_KEYSVALUES
value = [id, model];
}
}
return {value: value, done: false};
}
用盡後,移除對集合的參照,因此未來呼叫 next 方法時總是會傳回 done。
this._collection = void 0;
}
return {value: void 0, done: true};
};
Backbone View 幾乎更像是慣例,而非實際程式碼。View 僅僅是一個 JavaScript 物件,代表 DOM 中的 UI 邏輯區塊。這可能是單一項目、整個清單、側邊欄或面板,甚至包裝整個應用程式的周圍框架。將 UI 區塊定義為View 允許您宣告式地定義 DOM 事件,而不用擔心呈現順序… 且讓 view 能輕易對模型狀態中的特定變更做出反應。
建立 Backbone.View 會在 DOM 外部建立其初始元素,如果未提供現有元素…
var View = Backbone.View = function(options) {
this.cid = _.uniqueId('view');
this.preinitialize.apply(this, arguments);
_.extend(this, _.pick(options, viewOptions));
this._ensureElement();
this.initialize.apply(this, arguments);
};
快取正規表示式,用於分割 delegate
的金鑰。
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
要設定為屬性的 view 選項清單。
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
設定所有可繼承的Backbone.View 屬性和方法。
_.extend(View.prototype, Events, {
View 元素的預設 tagName
為 "div"
。
tagName: 'div',
jQuery delegate 用於元素查詢,範圍為目前 view 內的 DOM 元素。這應優先於全球查詢(如果可能)。
$: function(selector) {
return this.$el.find(selector);
},
preinitialize 預設為空函式。您可以使用函式或物件覆寫它。preinitialize 會在 View 中執行任何實例化邏輯之前執行
preinitialize: function(){},
Initialize 預設為空函式。使用您自己的初始化邏輯覆寫它。
initialize: function(){},
render 是您的 view 應該覆寫的核心函式,用於使用適當的 HTML 填入其元素 (this.el
)。慣例是render 始終傳回 this
。
render: function() {
return this;
},
透過將元素移出 DOM 並移除任何適用的 Backbone.Events 監聽器,移除此 view。
remove: function() {
this._removeElement();
this.stopListening();
return this;
},
移除此檢視的元素和所有附加的事件聆聽器。使用替代的 DOM 處理 API,公開給子類別使用。
_removeElement: function() {
this.$el.remove();
},
變更檢視的元素(this.el
屬性)並重新委派檢視的事件至新的元素。
setElement: function(element) {
this.undelegateEvents();
this._setElement(element);
this.delegateEvents();
return this;
},
使用指定的 el
為此檢視建立 this.el
和 this.$el
參照。el
可以是 CSS 選擇器或 HTML 字串、jQuery 內容或元素。子類別可以覆寫此項目以使用替代的 DOM 處理 API,且僅需要設定 this.el
屬性。
_setElement: function(el) {
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
this.el = this.$el[0];
},
設定回呼,其中 this.events
是
{“事件選擇器”: “回呼”}
{
'mousedown .title': 'edit',
'click .button': 'save',
'click .open': function(e) { ... }
}
配對的雜湊。回呼會繫結到檢視,並正確設定 this
。為了效率,使用事件委派。略過選擇器會將事件繫結到 this.el
。
delegateEvents: function(events) {
events || (events = _.result(this, 'events'));
if (!events) return this;
this.undelegateEvents();
for (var key in events) {
var method = events[key];
if (!_.isFunction(method)) method = this[method];
if (!method) continue;
var match = key.match(delegateEventSplitter);
this.delegate(match[1], match[2], method.bind(this));
}
return this;
},
將單一事件聆聽器新增至檢視的元素(或使用 selector
的子元素)。這僅適用於可委派的事件:在 Internet Explorer 中,不適用於 focus
、blur
,也不適用於 change
、submit
和 reset
。
delegate: function(eventName, selector, listener) {
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
清除先前由 delegateEvents
繫結至檢視的所有回呼。通常不需要使用此項目,但如果您有多個 Backbone 檢視附加至同一個 DOM 元素,則可能需要使用。
undelegateEvents: function() {
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
較精細的 undelegateEvents
,用於移除單一委派事件。selector
和 listener
都是選用的。
undelegate: function(eventName, selector, listener) {
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
產生要指定給檢視的 DOM 元素。使用替代的 DOM 處理 API,公開給子類別使用。
_createElement: function(tagName) {
return document.createElement(tagName);
},
確保檢視有 DOM 元素可以進行呈現。如果 this.el
是字串,請透過 $()
傳遞,取得第一個符合的元素,並重新指定給 el
。否則,從 id
、className
和 tagName
屬性建立元素。
_ensureElement: function() {
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
this.setElement(this._createElement(_.result(this, 'tagName')));
this._setAttributes(attrs);
} else {
this.setElement(_.result(this, 'el'));
}
},
從雜湊設定此檢視元素的屬性。使用替代的 DOM 處理 API,公開給子類別使用。
_setAttributes: function(attributes) {
this.$el.attr(attributes);
}
});
將 Backbone 類別方法代理至 Underscore 函式,在幕後包裝模型的 attributes
物件或集合的 models
陣列。
collection.filter(function(model) { return model.get(‘age’) > 10 }); collection.each(this.addView);
Function#apply
可能很慢,因此我們使用已知的方法的參數數量。
var addMethod = function(base, length, method, attribute) {
switch (length) {
case 1: return function() {
return base[method](this[attribute]);
};
case 2: return function(value) {
return base[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return base[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return base[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return base[method].apply(base, args);
};
}
};
var addUnderscoreMethods = function(Class, base, methods, attribute) {
_.each(methods, function(length, method) {
if (base[method]) Class.prototype[method] = addMethod(base, length, method, attribute);
});
};
支援 collection.sortBy('attr')
和 collection.findWhere({id: 1})
。
var cb = function(iteratee, instance) {
if (_.isFunction(iteratee)) return iteratee;
if (_.isObject(iteratee) && !instance._isModel(iteratee)) return modelMatcher(iteratee);
if (_.isString(iteratee)) return function(model) { return model.get(iteratee); };
return iteratee;
};
var modelMatcher = function(attrs) {
var matcher = _.matches(attrs);
return function(model) {
return matcher(model.attributes);
};
};
我們想要在集合上實作的 Underscore 方法。Backbone 集合的核心實用性有 90% 實際上是在這裡實作的
var collectionMethods = {forEach: 3, each: 3, map: 3, collect: 3, reduce: 0,
foldl: 0, inject: 0, reduceRight: 0, foldr: 0, find: 3, detect: 3, filter: 3,
select: 3, reject: 3, every: 3, all: 3, some: 3, any: 3, include: 3, includes: 3,
contains: 3, invoke: 0, max: 3, min: 3, toArray: 1, size: 1, first: 3,
head: 3, take: 3, initial: 3, rest: 3, tail: 3, drop: 3, last: 3,
without: 0, difference: 0, indexOf: 3, shuffle: 1, lastIndexOf: 3,
isEmpty: 1, chain: 1, sample: 3, partition: 3, groupBy: 3, countBy: 3,
sortBy: 3, indexBy: 3, findIndex: 3, findLastIndex: 3};
我們想要在模型上實作的 Underscore 方法,對應它們所使用的參數數量。
var modelMethods = {keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1};
將每個 Underscore 方法混合作為 Collection#models
的代理。
_.each([
[Collection, collectionMethods, 'models'],
[Model, modelMethods, 'attributes']
], function(config) {
var Base = config[0],
methods = config[1],
attribute = config[2];
Base.mixin = function(obj) {
var mappings = _.reduce(_.functions(obj), function(memo, name) {
memo[name] = 0;
return memo;
}, {});
addUnderscoreMethods(Base, obj, mappings, attribute);
};
addUnderscoreMethods(Base, _, methods, attribute);
});
覆寫此函式以變更 Backbone 將模型持續儲存在伺服器的方式。您會收到請求類型和有問題的模型。預設會對模型的 url()
提出 RESTful Ajax 請求。一些可能的自訂包括
setTimeout
將快速更新批次處理成單一請求。開啟 Backbone.emulateHTTP
以便將 PUT
和 DELETE
要求傳送為 POST
,其中包含 _method
參數(包含真實 HTTP 方法),以及所有要求,其中主體為 application/x-www-form-urlencoded
,而非 application/json
,其中模型在名為 model
的參數中。在與伺服器端語言(例如 PHP)介接時很有用,因為這些語言難以讀取 PUT
要求的主體。
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
預設選項,除非另有指定。
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
預設 JSON 要求選項。
var params = {type: type, dataType: 'json'};
確保我們有 URL。
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
確保我們有適當的請求資料。
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
對於較舊的伺服器,透過將請求編碼為 HTML 表單來模擬 JSON。
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
對於較舊的伺服器,透過使用 _method
和 X-HTTP-Method-Override
標頭來模擬 HTTP 方法,模擬 HTTP。
if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) {
params.type = 'POST';
if (options.emulateJSON) params.data._method = type;
var beforeSend = options.beforeSend;
options.beforeSend = function(xhr) {
xhr.setRequestHeader('X-HTTP-Method-Override', type);
if (beforeSend) return beforeSend.apply(this, arguments);
};
}
不要處理非 GET 請求的資料。
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
從 jQuery 傳遞 textStatus
和 errorThrown
。
var error = options.error;
options.error = function(xhr, textStatus, errorThrown) {
options.textStatus = textStatus;
options.errorThrown = errorThrown;
if (error) error.call(options.context, xhr, textStatus, errorThrown);
};
提出請求,允許使用者覆寫任何 Ajax 選項。
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
};
從 CRUD 對應到 HTTP 以供我們的預設 Backbone.sync
實作。
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
將 Backbone.ajax
的預設實作設定為透過 $
代理。如果您想使用其他函式庫,請覆寫此項目。
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
路由器將偽 URL 對應到動作,並在路由相符時觸發事件。建立新的路由器會設定其 routes
hash,如果未靜態設定。
var Router = Backbone.Router = function(options) {
options || (options = {});
this.preinitialize.apply(this, arguments);
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
用於比對命名參數部分和路由字串的 splatted 部分的快取正規表示式。
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
設定所有可繼承的 Backbone.Router 屬性和方法。
_.extend(Router.prototype, Events, {
preinitialize 預設為空函式。您可以使用函式或物件覆寫它。preinitialize 會在路由器中執行任何實例化邏輯之前執行。
preinitialize: function(){},
Initialize 預設為空函式。使用您自己的初始化邏輯覆寫它。
initialize: function(){},
route: function(route, name, callback) {
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
if (router.execute(callback, args, name) !== false) {
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
}
});
return this;
},
使用提供的參數執行路由處理常式。這是執行路由前設定或路由後清理的絕佳時機。
execute: function(callback, args, name) {
if (callback) callback.apply(this, args);
},
簡單的 Backbone.history
代理,用於將片段儲存到歷程記錄中。
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
return this;
},
將所有已定義的路由繫結到 Backbone.history
。我們必須反轉這裡路由的順序,以支援在路由對應中,最通用的路由可以在路由對應的底部定義的行為。
_bindRoutes: function() {
if (!this.routes) return;
this.routes = _.result(this, 'routes');
var route, routes = _.keys(this.routes);
while ((route = routes.pop()) != null) {
this.route(route, this.routes[route]);
}
},
將路由字串轉換為正規表示式,適合與目前的位址雜湊進行比對。
_routeToRegExp: function(route) {
route = route.replace(escapeRegExp, '\\$&')
.replace(optionalParam, '(?:$1)?')
.replace(namedParam, function(match, optional) {
return optional ? match : '([^/?]+)';
})
.replace(splatParam, '([^?]*?)');
return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
},
給定一個路由和與之相符的 URL 片段,傳回已萃取的已解碼參數陣列。空或未比對的參數將視為 null
,以標準化跨瀏覽器行為。
_extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1);
return _.map(params, function(param, i) {
不要解碼搜尋參數。
if (i === params.length - 1) return param || null;
return param ? decodeURIComponent(param) : null;
});
}
});
處理跨瀏覽器歷程記錄管理,基於 pushState 和真實 URL,或 onhashchange 和 URL 片段。如果瀏覽器不支援這兩種方式(例如舊版 IE),則改用輪詢。
var History = Backbone.History = function() {
this.handlers = [];
this.checkUrl = this.checkUrl.bind(this);
確保 History
可以用於瀏覽器外。
if (typeof window !== 'undefined') {
this.location = window.location;
this.history = window.history;
}
};
用於移除前導雜湊/斜線和尾端空白的快取正規表示式。
var routeStripper = /^[#\/]|\s+$/g;
用於移除前導和尾端斜線的快取正規表示式。
var rootStripper = /^\/+|\/+$/g;
用於移除雜湊的 URL 的快取正規表示式。
var pathStripper = /#.*$/;
歷程記錄處理是否已開始?
History.started = false;
設定所有可繼承的 Backbone.History 屬性和方法。
_.extend(History.prototype, Events, {
必要時,輪詢雜湊變更的預設間隔為每秒 20 次。
interval: 50,
我們是否位於應用程式根目錄?
atRoot: function() {
var path = this.location.pathname.replace(/[^\/]$/, '$&/');
return path === this.root && !this.getSearch();
},
路徑名稱是否與根目錄相符?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
var rootPath = path.slice(0, this.root.length - 1) + '/';
return rootPath === this.root;
},
location.pathname
中的 Unicode 字元會進行百分比編碼,因此會在比較前進行解碼。%25
不應解碼,因為它可能是編碼參數的一部分。
decodeFragment: function(fragment) {
return decodeURI(fragment.replace(/%25/g, '%2525'));
},
在 IE6 中,如果雜湊片段包含 ?
,則雜湊片段和搜尋參數會不正確。
getSearch: function() {
var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
return match ? match[0] : '';
},
取得真實雜湊值。無法直接使用 location.hash,因為 Firefox 中存在一個錯誤,導致 location.hash 始終會被解碼。
getHash: function(window) {
var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
取得路徑名稱和搜尋參數,但不包含根目錄。
getPath: function() {
var path = this.decodeFragment(
this.location.pathname + this.getSearch()
).slice(this.root.length - 1);
return path.charAt(0) === '/' ? path.slice(1) : path;
},
從路徑或雜湊中取得跨瀏覽器正規化的 URL 片段。
getFragment: function(fragment) {
if (fragment == null) {
if (this._usePushState || !this._wantsHashChange) {
fragment = this.getPath();
} else {
fragment = this.getHash();
}
}
return fragment.replace(routeStripper, '');
},
開始處理雜湊變更,如果目前的 URL 與現有路由相符,則傳回 true
,否則傳回 false
。
start: function(options) {
if (History.started) throw new Error('Backbone.history has already been started');
History.started = true;
找出初始設定。我們需要一個 iframe 嗎?是否需要 pushState … 是否可用?
this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root;
this._trailingSlash = this.options.trailingSlash;
this._wantsHashChange = this.options.hashChange !== false;
this._hasHashChange = 'onhashchange' in window && (document.documentMode === void 0 || document.documentMode > 7);
this._useHashChange = this._wantsHashChange && this._hasHashChange;
this._wantsPushState = !!this.options.pushState;
this._hasPushState = !!(this.history && this.history.pushState);
this._usePushState = this._wantsPushState && this._hasPushState;
this.fragment = this.getFragment();
將根目錄正規化,始終包含開頭和結尾的斜線。
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
如果同時要求雜湊變更和 pushState,則從雜湊變更轉換為 pushState,或反之亦然。
if (this._wantsHashChange && this._wantsPushState) {
如果我們一開始從啟用 pushState
的瀏覽器中取得路由,但目前在不支援它的瀏覽器中…
if (!this._hasPushState && !this.atRoot()) {
var rootPath = this.root.slice(0, -1) || '/';
this.location.replace(rootPath + '#' + this.getPath());
立即傳回,因為瀏覽器會重新導向到新的 URL。
return true;
或者如果我們一開始從基於雜湊的路由中取得路由,但目前在可以改用 pushState
的瀏覽器中…
} else if (this._hasPushState && this.atRoot()) {
this.navigate(this.getHash(), {replace: true});
}
}
如果瀏覽器不支援 hashchange
事件、HTML5 歷程記錄,或使用者需要 hashChange
但不需要 pushState
,則代理一個 iframe 來處理位置事件。
if (!this._hasHashChange && this._wantsHashChange && !this._usePushState) {
this.iframe = document.createElement('iframe');
this.iframe.src = 'javascript:0';
this.iframe.style.display = 'none';
this.iframe.tabIndex = -1;
var body = document.body;
如果文件尚未準備好,則在 IE < 9 中使用 appendChild
會擲回錯誤。
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
iWindow.document.open();
iWindow.document.close();
iWindow.location.hash = '#' + this.fragment;
}
為舊版瀏覽器新增一個跨平台的 addEventListener
shim。
var addEventListener = window.addEventListener || function(eventName, listener) {
return attachEvent('on' + eventName, listener);
};
根據我們使用 pushState 或雜湊,以及是否支援「onhashchange」,決定如何檢查 URL 狀態。
if (this._usePushState) {
addEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
if (!this.options.silent) return this.loadUrl();
},
停用 Backbone.history,可能暫時停用。在實際應用程式中沒有用,但可能對單元測試路由器有用。
stop: function() {
為舊版瀏覽器新增一個跨平台的 removeEventListener
shim。
var removeEventListener = window.removeEventListener || function(eventName, listener) {
return detachEvent('on' + eventName, listener);
};
移除視窗監聽器。
if (this._usePushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
removeEventListener('hashchange', this.checkUrl, false);
}
必要時清除 iframe。
if (this.iframe) {
document.body.removeChild(this.iframe);
this.iframe = null;
}
某些環境在清除未定義的間隔時會擲回錯誤。
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false;
},
當片段變更時,新增一個要測試的路由。稍後新增的路由可能會覆寫先前的路由。
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
檢查目前的 URL 是否已變更,如果已變更,則呼叫 loadUrl
,並在隱藏的 iframe 中進行正規化。
checkUrl: function(e) {
var current = this.getFragment();
如果使用者按下返回按鈕,則 iframe 的雜湊會已變更,我們應使用它來進行比較。
if (current === this.fragment && this.iframe) {
current = this.getHash(this.iframe.contentWindow);
}
if (current === this.fragment) {
if (!this.matchRoot()) return this.notfound();
return false;
}
if (this.iframe) this.navigate(current);
this.loadUrl();
},
嘗試載入目前的 URL 片段。如果某個路由成功搭配,則傳回 true
。如果沒有已定義的路由搭配該片段,則傳回 false
。
loadUrl: function(fragment) {
如果根目錄不搭配,則沒有路由可以搭配。
if (!this.matchRoot()) return this.notfound();
fragment = this.fragment = this.getFragment(fragment);
return _.some(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
}) || this.notfound();
},
當沒有路由可以搭配時,此方法會在內部呼叫以觸發 'notfound'
事件。它傳回 false
,以便可以在尾端位置使用。
notfound: function() {
this.trigger('notfound');
return false;
},
將片段儲存到雜湊歷程記錄,或如果傳遞了「取代」選項,則取代 URL 狀態。您負責事先正確地對片段進行 URL 編碼。
如果希望觸發路由回呼(通常不建議),則選項物件可以包含 trigger: true
,或者如果希望修改目前的 URL 而不在歷程記錄中新增項目,則可以包含 replace: true
。
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options};
標準化片段。
fragment = this.getFragment(fragment || '');
除非 _trailingSlash 為 true,否則移除根目錄上的尾部斜線
var rootPath = this.root;
if (!this._trailingSlash && (fragment === '' || fragment.charAt(0) === '?')) {
rootPath = rootPath.slice(0, -1) || '/';
}
var url = rootPath + fragment;
移除片段的查詢和雜湊以進行搭配。
fragment = fragment.replace(pathStripper, '');
解碼以進行搭配。
var decodedFragment = this.decodeFragment(fragment);
if (this.fragment === decodedFragment) return;
this.fragment = decodedFragment;
如果 pushState 可用,我們使用它來將片段設定為真正的 URL。
if (this._usePushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
如果雜湊變更尚未明確停用,請更新雜湊片段以儲存歷程記錄。
} else if (this._wantsHashChange) {
this._updateHash(this.location, fragment, options.replace);
if (this.iframe && fragment !== this.getHash(this.iframe.contentWindow)) {
var iWindow = this.iframe.contentWindow;
開啟和關閉 iframe 會欺騙 IE7 及更早版本在雜湊標籤變更時推入歷程記錄項目。當 replace 為 true 時,我們不希望這樣做。
if (!options.replace) {
iWindow.document.open();
iWindow.document.close();
}
this._updateHash(iWindow.location, fragment, options.replace);
}
如果您明確告訴我們您明確不想要基於雜湊變更的備用歷程記錄,則 navigate
會變成重新整理頁面。
} else {
return this.location.assign(url);
}
if (options.trigger) return this.loadUrl(fragment);
},
更新雜湊位置,取代目前的項目或在瀏覽器歷程記錄中新增新的項目。
_updateHash: function(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment);
} else {
某些瀏覽器要求 hash
包含開頭的 #。
location.hash = '#' + fragment;
}
}
});
建立預設的 Backbone.history。
Backbone.history = new History;
輔助函式,用於正確設定子類別的原型鏈。類似於 goog.inherits
,但使用原型屬性和類別屬性的雜湊來進行延伸。
var extend = function(protoProps, staticProps) {
var parent = this;
var child;
新子類別的建構函式由您定義(extend
定義中的「建構函式」屬性),或由我們預設為僅呼叫父建構函式。
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
如果提供,則將靜態屬性新增到建構函式。
_.extend(child, parent, staticProps);
設定原型鏈以繼承自 parent
,而不呼叫 parent
的建構函式,並新增原型屬性。
child.prototype = _.create(parent.prototype, protoProps);
child.prototype.constructor = child;
設定方便屬性,以防稍後需要父項目的原型。
child.__super__ = parent.prototype;
return child;
};
設定模型、集合、路由器、檢視和歷程記錄的繼承。
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
當需要 URL 但未提供時,擲回錯誤。
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
使用備用錯誤事件包裝可選的錯誤回呼。
var wrapError = function(model, options) {
var error = options.error;
options.error = function(resp) {
if (error) error.call(options.context, model, resp, options);
model.trigger('error', model, resp, options);
};
};
當事情出錯時,提供有用的資訊。此方法並非用於直接使用;它僅提供外部 debugInfo
函數必要的內省。
Backbone._debug = function() {
return {root: root, _: _};
};
return Backbone;
});