(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://backbone.npmjs.net.cn
(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
-ing 事件会依次触发所有回调。
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
对象充当全局事件总线,供那些想要在方便的位置使用全局“发布/订阅”的人使用。
_.extend(Backbone, Events);
Backbone 模型 是框架中的基本数据对象——通常代表服务器上数据库中表中的一行。一个离散的数据块和一堆有用的相关方法,用于对该数据执行计算和转换。
使用指定的属性创建一个新的模型。客户端 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 用于在本地识别模型。如果你遇到模型 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 或 undefined 的值,则返回 true
。
has: function(attr) {
return this.get(attr) != null;
},
对 underscore 的 _.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。对于确定视图的哪些部分需要更新和/或哪些属性需要持久化到服务器很有用。未设置的属性将设置为 undefined。你还可以传递一个属性对象来与模型进行比较,确定是否 会 发生更改。
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);
},
从服务器获取模型,将响应与模型的本地属性合并。任何已更改的属性都会触发“change”事件。
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;
如果我们不等待并且存在属性,则 save 充当 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};
将 插入
拼接到 数组
中,索引为 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];
};
定义集合的可继承方法。
_.extend(Collection.prototype, Events, {
集合的默认模型只是一个 **Backbone.Model**。在大多数情况下,应该覆盖它。
model: Model,
preinitialize
默认情况下是一个空函数。您可以用函数或对象覆盖它。preinitialize
将在集合中运行任何实例化逻辑之前运行。
preinitialize: function(){},
Initialize 默认是一个空函数。用你自己的初始化逻辑覆盖它。
initialize: function(){},
集合的 JSON 表示形式是模型属性的数组。
toJSON: function(options) {
return this.map(function(model) { return model.toJSON(options); });
},
默认情况下代理 Backbone.sync
。
sync: function() {
return Backbone.sync.apply(this, arguments);
},
向集合中添加一个模型或模型列表。模型
可以是 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);
}
查看是否需要排序,更新 长度
并插入新模型。
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});
除非静默,否则是时候触发所有适当的添加/排序/更新事件了。
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;
},
当您拥有比单独添加或删除更多的项目时,您可以使用新的模型列表重置整个集合,而不会触发任何细粒度的 添加
或 删除
事件。完成后触发 重置
。对于批量操作和优化很有用。
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);
根据 比较器
的类型运行排序。
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: 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;
},
内部方法,由 remove 和 set 共同调用。
_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 的可迭代协议。在现代 ES2015 浏览器中,此值位于 Symbol.iterator 中。
/* global Symbol */
var $$iterator = typeof Symbol === 'function' && Symbol.iterator;
if ($$iterator) {
Collection.prototype[$$iterator] = Collection.prototype.values;
}
CollectionIterator 实现了 JavaScript 的迭代器协议,允许在现代浏览器中使用 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;
所有迭代器本身都应该是可迭代的。
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 视图几乎更像是约定,而不是实际代码。视图只是一个 JavaScript 对象,它表示 DOM 中的 UI 的逻辑块。这可能是一个单独的项目、一个完整的列表、一个侧边栏或面板,甚至可能是围绕整个应用程序的框架。将 UI 的一部分定义为 **视图** 使您能够声明性地定义 DOM 事件,而无需担心渲染顺序……并使视图能够轻松地对模型状态的特定更改做出反应。
创建 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*(.*)$/;
要设置为属性的视图选项列表。
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
设置所有可继承的 **Backbone.View** 属性和方法。
_.extend(View.prototype, Events, {
视图元素的默认 tagName
是 "div"
。
tagName: 'div',
用于元素查找的 jQuery 代理,作用域为当前视图内的 DOM 元素。在可能的情况下,应优先于全局查找。
$: function(selector) {
return this.$el.find(selector);
},
preinitialize
默认情况下是一个空函数。您可以用函数或对象覆盖它。preinitialize
将在视图中运行任何实例化逻辑之前运行
preinitialize: function(){},
Initialize 默认是一个空函数。用你自己的初始化逻辑覆盖它。
initialize: function(){},
**render** 是视图应该覆盖的核心函数,以便使用适当的 HTML 填充其元素 (this.el
)。约定是 **render** 始终返回 this
。
render: function() {
return this;
},
通过将元素从 DOM 中取出并删除任何适用的 Backbone.Events 监听器来删除此视图。
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;
},
向视图的元素(或使用 选择器
的子元素)添加单个事件监听器。这仅适用于可委托的事件:不包括 focus
、blur
以及 Internet Explorer 中的 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
。选择器
和 监听器
都是可选的。
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);
};
};
我们希望在 Collection 上实现的 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};
我们希望在 Model 上实现的 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 方法。
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
哈希表,如果它没有被静态设置。
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);
};
用于匹配命名参数部分和路由字符串中 splat 部分的缓存正则表达式。
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] : '';
},
获取真实的哈希值。由于 Firefox 中的错误,无法直接使用 location.hash,因为 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, '/');
在 hashChange 和 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;
},
将片段保存到哈希历史记录中,或者如果传递了 'replace' 选项,则替换 URL 状态。您负责提前对片段进行正确的 URL 编码。
选项对象可以包含 trigger: true
,如果您希望触发路由回调(通常不希望这样做),或者 replace: true
,如果您希望修改当前 URL 而不向历史记录添加条目。
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);
}
如果您告诉我们您明确不希望回退基于 hashchange 的历史记录,那么 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
定义中的“constructor”属性),要么由我们默认设置为简单地调用父构造函数。
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;
});