BackboneJS 源码注释
Backbone 作者在源码中做了很好的注释,这里只是锦上添花,补充一些个人的理解而已。
// Backbone.js 1.2.3
// (c) 2010-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Backbone may be freely distributed under the MIT license.
// For all details and documentation:
// http://backbonejs.org
(function(factory) {
// Establish the root object, `window` (`self`) in the browser, or `global` on the server.
// We use `self` instead of `window` for `WebWorker` support.
var root = (typeof self == 'object' && self.self == self && self) ||
(typeof global == 'object' && global.global == global && global);
// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {
define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
// Export global even in AMD case in case this script is loaded with
// others that may still expect a global Backbone.
root.Backbone = factory(root, exports, _, $);
});
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore'), $;
try { $ = require('jquery'); } catch(e) {}
factory(root, exports, _, $);
// Finally, as a browser global.
} else {
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$));
}
}(function(root, Backbone, _, $) {
// Initial Setup
// -------------
// Save the previous value of the `Backbone` variable, so that it can be
// restored later on, if `noConflict` is used.
var previousBackbone = root.Backbone;
// Create a local reference to a common array method we'll want to use later.
var slice = Array.prototype.slice;
// Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.2.3';
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns
// the `$` variable.
Backbone.$ = $;
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable
// to its previous owner. Returns a reference to this Backbone object.
Backbone.noConflict = function() {
root.Backbone = previousBackbone;
return this;
};
// Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option
// will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and
// set a `X-Http-Method-Override` header.
Backbone.emulateHTTP = false;
// Turn on `emulateJSON` to support legacy servers that can't deal with direct
// `application/json` requests ... this will encode the body as
// `application/x-www-form-urlencoded` instead and will send the model in a
// form param named `model`.
Backbone.emulateJSON = false;
// Proxy Backbone class methods to Underscore functions, wrapping the model's
// `attributes` object or collection's `models` array behind the scenes.
//
// collection.filter(function(model) { return model.get('age') > 10 });
// collection.each(this.addView);
//
// `Function#apply` can be slow so we use the method's arg count, if we know it.
var addMethod = function(length, method, attribute) {
switch (length) {
case 1: return function() {
return _[method](this[attribute]);
};
case 2: return function(value) {
return _[method](this[attribute], value);
};
case 3: return function(iteratee, context) {
return _[method](this[attribute], cb(iteratee, this), context);
};
case 4: return function(iteratee, defaultVal, context) {
return _[method](this[attribute], cb(iteratee, this), defaultVal, context);
};
default: return function() {
var args = slice.call(arguments);
args.unshift(this[attribute]);
return _[method].apply(_, args);
};
}
};
// 添加 underscore 方法
var addUnderscoreMethods = function(Class, methods, attribute) {
_.each(methods, function(length, method) {
if (_[method]) Class.prototype[method] = addMethod(length, method, attribute);
});
};
// Support `collection.sortBy('attr')` and `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);
};
};
// Backbone.Events - 事件
// -------------------------
// A module that can be mixed in to *any object* in order to provide it with
// a custom event channel. You may bind a callback to an event with `on` or
// remove with `off`; `trigger`-ing an event fires all callbacks in
// succession.
//
// var object = {};
// _.extend(object, Backbone.Events);
// object.on('expand', function(){ alert('expanded'); });
// object.trigger('expand');
//
// Events 所有事件行为都保存在 obj._events 属性中,
// 无论是 on 还是 listenTo,事件行为都是保存在事件触发者身上,
// listenTo 其实只是 on 的另一种调用形式。
// 所以,当 events.off() 方法会解绑所有事件,同时也会解除所有被监听关系。(即监听者无法再继续监听)
// events.stopListening() 停止监听所有事件,从被监听者身上解除事件处理。
//
// obj._listeningTo 是保存对被监听对象的引用。
// obj._listenId 是每个事件触发者自己身份的 ID,当自己被其他人监听时,用以标识自己身份。
// obj._listeners 是所有对自己进行监听的对象引用映射。
//
// Backbone.Events 的 listenTo 与 stopListening 方法实现原理:
// 例如监听者 listener 和被监听者 listenee。
//
// `listener.listenTo(listenee, 'any', callback);`
//
// 整个监听过程大致可以分为三个部分:
// 1. 生成监听关系表:
//
// ```
// {
// count: Int, // 监听次数,当 count 为 0 时,表示二者不再存在任何监听关系,从双方删除监听关系。
// id: String, // listener._listenId,标识监听者身份。
// listeningTo: Object, // listener._listeningTo,保存所有监听关系表(以被监听者 ID 作为主键)
// obj: listenee, // listenee,被监听者。
// objId: String // listenee._listenId,被监听者 ID。
// }
// ```
//
// 2. 在 listenee 中保存事件以及事件回调,即在 listenee._events['any'] 队列中推入事件处理关系表。
//
// ```
// {
// callback: Function, // 回调函数
// context: Object,
// ctx: Object,
// listening: Object // listening 就是监听关系表
// }
// ```
//
// 3. 在双方各自添加监听关系:
// 在 listener 方面,以 listenee._listenId 为主键,保存在 listener._listeningTo 字段。
// 在 listenee 方面,以 listener._listenId 为主键,保存在 listenee._listeners 字段。
// 即以下等式是成立的 `listener._listeningTo[listenee._listenId] === listenee._listeners[listener._listenId]`。
//
// `listener.stopListening(listenee, 'any', callback);`
//
// 停止监听过程其实是通过 `listenee.off('any', callback, listener)` 来实现,这一过程包括:
// 从 listenee._events 移除事件处理关系表。
// 通过监听关系表里的 count 字段减一并判断是否为 0,从而决定是否要从双方移除监听关系。
var Events = Backbone.Events = {};
// Regular expression used to split event strings.
// 正则表达式:多个事件名以空格分隔。
var eventSplitter = /\s+/;
// Iterates over the standard `event, callback` (as well as the fancy multiple
// space-separated events `"change blur", callback` and jQuery-style event
// maps `{event: callback}`).
//
// 遍历定义的事件。
//
// iteratee 是迭代函数,即 onApi, offApi, triggerApi, onceMap 函数
// eventsApi 作用是将 events, name, callback, opts 参数整理成标准格式传递给 iteratee 调用。
var eventsApi = function(iteratee, events, name, callback, opts) {
var i = 0, names;
if (name && typeof name === 'object') {
// Handle event maps.
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)) {
// Handle space separated event names by delegating them individually.
for (names = name.split(eventSplitter); i < names.length; i++) {
events = iteratee(events, names[i], callback, opts);
}
} else {
// Finally, standard events.
// events 事件映射表;
// name 事件名称;
// callback 事件处理函数;
// opts 额外参数
events = iteratee(events, name, callback, opts);
}
return events;
};
// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
// 绑定事件
Events.on = function(name, callback, context) {
return internalOn(this, name, callback, context);
};
// Guard the `listening` argument from the public API.
// 内部绑定事件函数,它是 Events.on, Events.listenTo 公开 API 内部真正实现事件绑定的函数
// 因此它比 Events.on, Events.listenTo 多一个参数 listening,
// 该参数为真,表示它正实现 listenTo 方法;否则表示它正式实现 on 方法。
var internalOn = function(obj, name, callback, context, listening) {
// 执行 eventsApi
obj._events = eventsApi(onApi, obj._events || {}, name, callback, {
context: context,
ctx: obj,
listening: listening
});
// 如果当前是实现 listenTo 方法,需要在被监听者的 _listeners 中,添加监听者的引用关系。
if (listening) {
var listeners = obj._listeners || (obj._listeners = {});
// listening.id 是监听者的 _listenId。
listeners[listening.id] = listening;
}
return obj;
};
// Inversion-of-control versions of `on`. Tell *this* object to listen to
// an event in another object... keeping track of what it's listening to
// for easier unbinding later.
// Events.on 操作的逆操作,表示监听另一个对象的事件,并保持对该对象的引用,以便解绑事件。
Events.listenTo = function(obj, name, callback) {
// 如果 obj 为否,则终止 listenTo 操作。
if (!obj) return this;
// 被监听对象应该有一个唯一的监听 ID,即 _listenId,用以标识被监听者身份。
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
// _listeningTo 是监听行为映射表,该表用以保存所有被监听者的引用。
var listeningTo = this._listeningTo || (this._listeningTo = {});
var listening = listeningTo[id];
// This object is not listening to any other events on `obj` yet.
// Setup the necessary references to track the listening callbacks.
// 如果被监听者是首次被当前监听者监听,应初始化监听引用。
if (!listening) {
// 监听者的监听 ID
var thisId = this._listenId || (this._listenId = _.uniqueId('l'));
// 监听引用保存的字段:
// obj: 被监听对象。
// objId: 被监听对象的监听 ID。
// id: 监听者监听 ID。
// listeningTo: 监听映射表。
// count: 监听者对被监听者监听的次数
listening = listeningTo[id] = {obj: obj, objId: id, id: thisId, listeningTo: listeningTo, count: 0};
}
// Bind callbacks on obj, and keep track of them on listening.
internalOn(obj, name, callback, this, listening);
return this;
};
// The reducing API that adds a callback to the `events` object.
// 绑定事件
var onApi = function(events, name, callback, options) {
// 只有给定事件处理函数才进行事件绑定
if (callback) {
// handlers 是事件处理函数组成的数组
var handlers = events[name] || (events[name] = []);
// context 事件处理函数上下文(用户给出),ctx 事件触发者(默认上下文),listening 监听引用关系表
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;
};
// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
// 解绑事件
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;
};
// Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
// 停止监听
Events.stopListening = function(obj, name, callback) {
// 被监听者引用表
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
// 需要被停止监听者 ID 集合(没有指定被监听者,则默认所有被监听者)
var ids = obj ? [obj._listenId] : _.keys(listeningTo);
// 遍历被停止监听者 ID,逐个解除监听。
for (var i = 0; i < ids.length; i++) {
var listening = listeningTo[ids[i]];
// If listening doesn't exist, this object is not currently
// listening to obj. Break out early.
// 如果 listening 不存在,表示当前没有监听行为。
if (!listening) break;
// 被监听者从自身解除事件行为。
listening.obj.off(name, callback, this);
}
// 当没有监听任何对象时,将 _listeningTo 属性置为 void 0。
if (_.isEmpty(listeningTo)) this._listeningTo = void 0;
return this;
};
// The reducing API that removes a callback from the `events` object.
var offApi = function(events, name, callback, options) {
// events 不存在,终止 off 操作
if (!events) return;
var i = 0, listening;
// context 指定上下文,listeners 监听者
var context = options.context, listeners = options.listeners;
// Delete all events listeners and "drop" events.
// 没有给定任何事件名、事件回调或上下文,则移除所有监听者,以及事件。
if (!name && !callback && !context) {
// 生成所有监听者 id。
var ids = _.keys(listeners);
// 遍历所有监听者 id,逐一接触引用关系
for (; i < ids.length; i++) {
listening = listeners[ids[i]];
delete listeners[listening.id]; // 移除监听者引用
delete listening.listeningTo[listening.objId]; // 移除监听关系
}
return;
}
// 如果没有指定事件名称,则移除全部事件
var names = name ? [name] : _.keys(events);
for (; i < names.length; i++) {
name = names[i];
var handlers = events[name];
// Bail out if there are no events stored.
// 如果没有回调函数,终止本次循环
if (!handlers) break;
// Replace events if there are any remaining. Otherwise, clean up.
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 {
listening = handler.listening;
if (listening && --listening.count === 0) {
delete listeners[listening.id];
delete listening.listeningTo[listening.objId];
}
}
}
// Update tail event if the list has any events. Otherwise, clean up.
if (remaining.length) {
events[name] = remaining;
} else {
delete events[name];
}
}
if (_.size(events)) return events;
};
// Bind an event to only be triggered a single time. After the first time
// the callback is invoked, its listener will be removed. If multiple events
// are passed in using the space-separated syntax, the handler will fire
// once for each event, not once for a combination of all events.
Events.once = function(name, callback, context) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.off, this));
return this.on(events, void 0, context);
};
// Inversion-of-control versions of `once`.
Events.listenToOnce = function(obj, name, callback) {
// Map the event into a `{event: once}` object.
var events = eventsApi(onceMap, {}, name, callback, _.bind(this.stopListening, this, obj));
return this.listenTo(obj, events);
};
// Reduces the event callbacks into a map of `{event: onceWrapper}`.
// `offer` unbinds the `onceWrapper` after it has been called.
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 one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
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;
};
// Handles triggering the appropriate event callbacks.
var triggerApi = function(objEvents, name, cb, 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;
};
// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
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;
}
};
// Aliases for backwards compatibility.
// bind 作为 on 别名,unbind 作为 off 别名。(为向后兼容)
Events.bind = Events.on;
Events.unbind = Events.off;
// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);
// Backbone.Model(模型)
// --------------------------
// Backbone **Models** are the basic data object in the framework --
// frequently representing a row in a table in a database on your server.
// A discrete chunk of data and a bunch of useful, related methods for
// performing computations and transformations on that data.
// Create a new model with the specified attributes. A client id (`cid`)
// is automatically generated and assigned for you.
//
// 默认的 Model 构造函数主要做三件事:
// 1. 为实例设置 cid 属性;
// 2. 为实例设置 attributes;
// 3. 调用实例的 initialize 方法完成初始化。
//
// 注意:
// 如果给定 attributes,它是通过 set 方法添加到 model 的 attributes。
// 并且是早于 initialize 方法调用。
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
// 生成唯一 cid,cid 表示 client id,是指在本地模型变量的标识,而不是模型所代表数据的唯一标识。
this.cid = _.uniqueId(this.cidPrefix);
this.attributes = {};
// 如果指定了 collection,则直接绑定到模型上。
if (options.collection) this.collection = options.collection;
// 默认初始化设置 attributes 是不经过 parse 方法的(parse 方法只有在 fetch/save 等同步数据时才调用)
// 如果指定 options.parse 为真,则初始化时调用 parse 方法解析 attrs。
if (options.parse) attrs = this.parse(attrs, options) || {};
// 初始化 attributes
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
// 调用 set 方法设置初始属性。
// 不用担心 set 会触发 change 事件,因为此时还没有调用 initialize 方法
// 所以通常说来,此时你还来不及绑定任何事件。
this.set(attrs, options);
// 调用 set 方法会导致 this.changed 发生变化,
// Jeremy Ashkenas 的意图是初始化的 Model 不应该含有变化的属性(因为一切都是初始的)
// 所以需要重新将 this.changed 修改为空对象。
// 注意:当设置初始 attributes 时,甚至都还没有调用 initialize。
this.changed = {};
// 调用初始化方法 initialize。
this.initialize.apply(this, arguments);
};
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, {
// 属性哈希,用以保存模型发生变化的属性哈希(只有 set 操作会产生旧属性哈希)
changed: null,
// set 操作前验证属性哈希合法性,
// 如果验证失败,本属性保存验证失败的结果(model.validate 返回值),
// 否则该属性会被重置为 null。
validationError: null,
// The default name for the JSON `id` attribute is `"id"`. MongoDB and
// CouchDB users may want to set this to `"_id"`.
idAttribute: 'id',
// The prefix is used to create the client id which is used to identify models locally.
// You may want to override this if you're experiencing name clashes with model ids.
cidPrefix: 'c',
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// 默认实现的 toJSON 方法是复制一份 attributes
// 但你可以覆写该方法,该方法参数 options 或许用得上。
// 该 options 与 fetch 方法的 options 参数相同,在未指定 HTTP 请求 data 时,
// Backbone.sync 会默认使用 `model.toJSON(options)` 来生成 data。
toJSON: function(options) {
return _.clone(this.attributes);
},
// Proxy `Backbone.sync` by default -- but override this if you need
// custom syncing semantics for *this* particular model.
//
// 模型同步数据,默认委托 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));
},
// 返回 true,如果模型指定 attribute 不为 null 或 undefined。
has: function(attr) {
return this.get(attr) != null;
},
// 委托 _.iteratee 来判断给定的 attrs 是否是模型 attributes 子集
// 根据传入 iteratee 参数不同,iteratee 具体实现也不同。
// 1. attrs 为 void 0。
// 相当于 _.identity(this.attributes),返回结果为 true。
// 2. attrs 为函数。
// 相当于 attrs(this.attributes);
// 3. attrs 为对象。
// 相当于 _.matcher(attrs)(this.attributes),判断 attrs 是否是 attributes 子集。
// 4. 其他(主要是指 string)
// 相当于 _.property(attrs)(this.attributes);
matches: function(attrs) {
return !!_.iteratee(attrs, this)(this.attributes);
},
// 设置模型属性哈希,触发 `change` 事件。
// 本方法是模型对象的核心操作,更新模型数据并将属性状态变化通知给外部。
// Backbone.Model 所有更新 attributes 的操作都是通过 set 方法完成,
// 例如初始化 initialize(attributes), fetch, save 等。
// 操作成功返回模型对象自身,操作失败返回 false。
// 注意:
// options.slient 为 true,只是表示本次 set 操作不触发 `change` 事件。
// 但仍然会更新模型的 `this.changed`, `this._previousAttributes` 属性,
// 因此在调用 `this.hasChanged()`, `this.changedAttributes()` ,`this.previous()`, `this.previousAttributes()` 方式时,
// 仍然可以识别中属性哈希的变化。
set: function(key, val, options) {
// 未指定属性名称的操作属于无效操作。
// 例如:`model.set()` 或 `model.set(null, options)`。
// 因此当需要调用 model.parse 方法时,
// 返回值为 null 或 undefined 将导致模型不设置任何属性哈希。
if (key == null) return this;
// 将 `model.set(key, value, options)` 转换为 `model.set({key: value}, options)` 风格。
var attrs;
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
options || (options = {});
// 正式设置属性哈希前,先验证输入参数。
// 如果要求验证数据,但验证数据失败,则中止 set 操作。
// this._validate 方法是通过 this.validate 方法来实现的,
// 只有定义了 this.validate 方法,才会进行验证,否则默认验证成功。
// 如果 this.validate 返回值为真,则表示验证失败(返回值就是验证失败原因 this.validationError),
// 否则验证成功,this.validationError 值设置为 null。
if (!this._validate(attrs, options)) return false;
// Extract attributes and options.
//
// 如果 unset 为 true,则从 attributes 中移除 key。
// 注意:
// 只有 key 存在于 attributes 中,且 value 不等于 attributes[key] 时,
// 当 key 被移除时才会触发 `change:key` 事件。
var unset = options.unset;
var silent = options.silent; // 如果为 true,不触发任何 `change` 事件。
var changes = []; // 发生变化属性名称列表
// 如果为 true,表示模型处于 set 操作中。
// 因为 set 操作可以内嵌在 set 中,this._changing 相当于操作锁。
// 而局部变量 changing 可以作为主动 set 的标识,
// 因为只有主动 set 的 changing 此时为 false,而递归 set 中的 changing 都是 true。
var changing = this._changing;
this._changing = true;
// 如果 set 操作未锁定,则设置相关属性
if (!changing) {
this._previousAttributes = _.clone(this.attributes); // 保存操作前的属性哈希副本
this.changed = {}; // (初始)设置变化属性哈希
}
var current = this.attributes; // 当前属性哈希
var changed = this.changed; // 当前变化属性哈希
var prev = this._previousAttributes; // 操作前属性哈希
// 遍历输入哈希,更新或删除哈希值
for (var attr in attrs) {
val = attrs[attr];
// 当前属性值不等于输入属性值时,在变化属性名列表中记录属性名称
if (!_.isEqual(current[attr], val)) changes.push(attr);
// 操作前属性值不等于输入属性值时,记录变化属性值,否则移除变化属性名。
// (因为 set 可以内嵌,this.changed 保存所有内嵌 set 操作结束后的属性变化状态)
if (!_.isEqual(prev[attr], val)) {
changed[attr] = val;
} else {
delete changed[attr];
}
// 如果 options.unset 为真,则从当前属性哈希中移除属性,否则更新当前属性哈希。
unset ? delete current[attr] : current[attr] = val;
}
// 更新模型 id,因为 set 可能会更改 idAttribute 指定的主键值。
this.id = this.get(this.idAttribute);
// Trigger all relevant attribute changes.
// 如果 set 不是静默操作,则需要通知第三方自身属性的变化。
if (!silent) {
// 如果变化属性名称列表不为空,则逐一触发 `change:key` 事件。
// 并且将输入 options 设置为 this._pending。
// `this._pending` 可以用来缓存输入 options,
// 当在递归 set 中有属性变化时,它可以不断被改写。
// 但只有在主动 set 中临近操作结束时被读取。
if (changes.length) this._pending = options;
for (var i = 0; i < changes.length; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
}
// You might be wondering why there's a `while` loop here. Changes can
// be recursively nested within `"change"` events.
//
// changing 为真,表示本次 set 为递归操作,主动 set 操作尚未结束,立即返回。
if (changing) return this;
// 本行以下代码只有在主动 set 操作中才会执行。
// 如果非静默 set,则需要触发 `change` 事件。
if (!silent) {
// 当 this._pending 为真时,表示有属性变化,需要触发 `change` 事件。
// 并且 this._pending 值就是输入参数 options。
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false; // 重置为 false 表示属性没有变化了。
this._changing = false; // 设置为 false 表示主动 set 操作结束。
return this;
},
// 从模型属性哈希中移除属性,并触发 `change` 事件。
// (通过调用 set 方法实现)
unset: function(attr, options) {
return this.set(attr, void 0, _.extend({}, options, {unset: true}));
},
// 从模型属性哈希中移除所有属性,触发 `change` 事件。
// (通过调用 set 方法实现)
clear: function(options) {
var attrs = {};
for (var key in this.attributes) attrs[key] = void 0;
return this.set(attrs, _.extend({}, options, {unset: true}));
},
// 判断模型对象属性哈希在最后一次 `set` 操作时,是否发生了变化。
// 或者判断指定的属性在最后一次 `set` 操作时是否发生了变化。
// 在 set 操作时,使用 options.silent = true 不影响本函数的判断结果。
hasChanged: function(attr) {
if (attr == null) return !_.isEmpty(this.changed);
return _.has(this.changed, attr);
},
// 本方法有两个用途:
// 1. 当不传入任何参数时(或 diff 为否),判断最后一次 set 操作,属性哈希是否发生变化。
// 如果发生变化,返回变化属性哈希,否则返回 false。
//
// 2. 传一个 Object 对象作为 diff 参数,将其与模型当前属性哈希进行对比,
// 筛选出于不同于当前属性哈希的属性,如果有筛选结果,则返回筛选结果,否则返回 false。
// 使用 `model.changedAttributes(someObject)` 可以(预先)判断出 set 哪些值会导致模型的属性哈希发生变化。
changedAttributes: function(diff) {
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false;
var old = this._changing ? this._previousAttributes : this.attributes;
var changed = {};
for (var attr in diff) {
var val = diff[attr];
if (_.isEqual(old[attr], val)) continue;
changed[attr] = val;
}
return _.size(changed) ? changed : false;
},
// 返回最后一次 set 之前的指定属性值(无论该属性是否发生过变化)。
// 不传入参数,或者模型没有进行过 set 操作,返回 null;
previous: function(attr) {
if (attr == null || !this._previousAttributes) return null;
return this._previousAttributes[attr];
},
// 返回最后一次 set 前的属性哈希。
// 如果没有 set 过,则返回 null。
previousAttributes: function() {
return _.clone(this._previousAttributes);
},
// Fetch the model from the server, merging the response with the model's
// local attributes. Any changed attributes will trigger a "change" event.
// fetch 方法主要用于从远端读取数据同步到本地 attributes,如果属性值发生变化,触发 `change` 事件。
// Backbone 本意是为 REST API 而设计,但也可以兼容非 REST API。
// 使用非 REST API 时,应该改写 parse 方法,再调用 fetch 方法。
fetch: function(options) {
// 远端的响应结果默认需要通过 parse 方法解析才能 set,
// 可以在 options 中指定 parse 为 false 来解除这一逻辑。
options = _.extend({parse: true}, options);
var model = this;
// 封装 success 操作
// 无论 options 中是否指定 success 回调,xhr 请求成功后都会有一次 success 回调。
// 如果有 options.success 回调函数,回调函数会在封装的 success 回调用执行。
var success = options.success;
options.success = function(resp) {
// 如果要求 parse 为真,则远程返回值必须经过 parse 方法解析,否则远程返回值就是响应数据。
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
// model.set 方法只有在 validate 失败时才会返回 false。
// 如果验证失败,则不会进行具有实际意义的 set 操作。
// 并且触发 invalid 事件。
if (!model.set(serverAttrs, options)) return false;
// 如果 set 操作成功,则继续调用原本计划的 success 回调函数。
// 注意:
// 此处的 success 回调与原生 jQuery ajax success 回调稍微不同的是,
// 它的上下文由 options.context 指定。
if (success) success.call(options.context, model, resp, options);
// 执行完 success 回调后触发 sync 事件。
model.trigger('sync', model, resp, options);
};
// 封装 options.error 回调,确保 xhr 失败时,触发 model 的 error 事件。
wrapError(this, options);
// read 远程数据,默认使用 this.sync 方法实现,
// this.sync 默认使用 Backbone.sync 方法实现,
// Backbone.sync 默认使用 Backbone.$ 方法实现,
// Backbone.$ 默认使用 jQuery.ajax 方法实现。
return this.sync('read', this, options);
},
// Set a hash of model attributes, and sync the model to the server.
// If the server returns an attributes hash that differs, the model's
// state will be `set` again.
//
// 设置 model 的 attributes,并且将 attributes 同步到远端。
// 如果远端返回的响应值(通过 parse 方法解析后)不同于 attributes,
// 则再次执行 set 操作。
save: function(key, val, options) {
// Handle both `"key", value` and `{key: value}` -style arguments.
// 处理不同传参方式。
var attrs;
if (key == null || typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
}
// 默认要求进行 validate 和 parse 操作。
options = _.extend({validate: true, parse: true}, options);
// 是否等待服务器响应再进行 set 操作的标识。(默认不等待)
// 等待服务器响应与否的区别是:
// 一个先 set,后同步数据。
// 一个是先同步数据,然后 set。
var wait = options.wait;
// If we're not waiting and attributes exist, save acts as
// `set(attr).save(null, opts)` with validation. Otherwise, check if
// the model will be valid when the attributes, if any, are set.
// 如果 attrs 为真,且无需等待服务器响应,则立即使用 attrs 进行 set 操作。
// 注意:
// 如果不等待服务器响应,set 操作一旦成功会立即触发 `change` 事件,
// 但随后的服务器响应值会被重新 set 一次,有可能会 set 失败。
if (attrs && !wait) {
// 如果 set 操作失败(即 validate 失败),立即返回 false(结束 save 操作)。
// 注意:什么要求 attrs 也为真才进行 set 操作?
// 如果不限制 attrs 为真,那么 set 操作会默认成功,则将导致 save 操作不会终止。
// 那么 save 会将未做任何修改的 attributes 再次同步到远端,这样不符合 save 操作的意图。
if (!this.set(attrs, options)) return false;
} else {
// 验证 attrs,验证失败则立即终止 save 操作。
if (!this._validate(attrs, options)) return false;
}
// After a successful server-side save, the client is (optionally)
// updated with the server-side state.
//
// 以下逻辑与 fetch 逻辑相似,开始进行数据同步相关操作。
var model = this;
var success = options.success;
var attributes = this.attributes;
// 封装 success 回调
options.success = function(resp) {
// Ensure attributes are restored during synchronous saves.
model.attributes = attributes;
// 解析远端响应值
var serverAttrs = options.parse ? model.parse(resp, options) : resp;
// 如果当前 save 操作是需要等待服务器响应的,则合并 attrs 和 serverAttrs 属性,
// 然后再进行 set 操作。
if (wait) serverAttrs = _.extend({}, attrs, serverAttrs);
// 无论是否要求 save 操作等待服务器响应,如果响应值存在(经过可能的 parse 操作后,解析结果不为 null 或 undefined),
// 则进行 set 操作,set 操作失败(验证失败)会立即终止 save 操作。
if (serverAttrs && !model.set(serverAttrs, options)) return false;
// set 成功后,执行可能计划的 success,然后触发 sync 事件。
if (success) success.call(options.context, model, resp, options);
model.trigger('sync', model, resp, options);
};
// 封装 error 回调
wrapError(this, options);
// Set temporary attributes if `{wait: true}` to properly find new ids.
// 设置临时的 attributes,因为在 Backbone.sync 操作中可能需要将 attributes 同步到远端。
// 注意:此处是直接修改 attributes,而不是通过 set 操作进行修改,因此不会触发任何事件。
if (attrs && wait) this.attributes = _.extend({}, attributes, attrs);
// 根据模型状态选择 REST API 的提交方式
var method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update');
// 如果是 patch 操作,则将 attrs 保存在 options.attrs 中
// (此处保存 attrs 的意图不是很明确,难道只是为了记录下 patch 的数据,以便满足开发者的个性化操作?)
if (method === 'patch' && !options.attrs) options.attrs = attrs;
var xhr = this.sync(method, this, options);
// Restore attributes.
// 立刻恢复模型应该拥有的 attributes。
this.attributes = attributes;
return xhr;
},
// Destroy this model on the server if it was already persisted.
// Optimistically removes the model from its collection, if it has one.
// If `wait: true` is passed, waits for the server to respond before removal.
// 销毁模型(并同步从远端销毁)
// 如果 options.wait 为真,则等待远端同步成功后,再销毁模型。
// destroy 操作主要实现:
// 1. stopListening 所有事件(不包括自身的 on 事件)
// 2. 触发 destroy 事件(通知 collection 将自己从 collection 中移除)
// 3. (可选)同步从远端删除数据。(根据 model.isNew() 判断是否要触发 sync 事件)
//
// 注意:
// Backbone.sync 期待的是 RESTFUL API,如果使用 emulatedHTTP,
// destroy 操作 success 会在 XHR 请求成功后立即执行,
// 即 XHR 请求成功,即视为远端删除数据成功。
// 因此在不重写 destroy 的方法前提下,要求远端接口响应必需以 HTTP Status Code(200 或 404)作为操作成功失败的标识,
// 而不能在响应的 data 中约定操作成功失败代号。
destroy: function(options) {
options = options ? _.clone(options) : {};
var model = this;
var success = options.success;
var wait = options.wait;
// 销毁模型(停止监听事件,触发 destroy 事件)
var destroy = function() {
model.stopListening();
model.trigger('destroy', model, model.collection, options);
};
// 封装 success 回调
// 该回调会在请求成功后立即执行,请求成功即被视为操作成功。
options.success = function(resp) {
if (wait) destroy();
if (success) success.call(options.context, model, resp, options);
// 如果 model.isNew() 为假,才有可能会触发 sync 事件。
if (!model.isNew()) model.trigger('sync', model, resp, options);
};
var xhr = false;
// 如果模型数据不存在于远端(按照 Backbone 设计理论),
// 则无需与远端进行数据同步操作,直接执行 success 回调。(理论上也不触发 sync 事件)
if (this.isNew()) {
_.defer(options.success);
} else {
// 封装异常回调
wrapError(this, options);
// 与远端同步
xhr = this.sync('delete', this, options);
}
// 如果不等待,则立即销毁模型。
if (!wait) destroy();
return xhr;
},
// Default URL for the model's representation on the server -- if you're
// using Backbone's restful methods, override this to change the endpoint
// that will be called.
// 生成 model 进行 sync 时的 URL(适用于 RESTFUL API)
// 默认的 url 方法主要适用于 RESTFUL API,自动生成 URL。
// 对于非 RESTFUL API,最好重写该方法。
url: function() {
var base =
_.result(this, 'urlRoot') ||
_.result(this.collection, 'url') ||
urlError();
if (this.isNew()) return base;
var id = this.get(this.idAttribute);
// 自动补齐 base 末尾的 `/` 符号,然后追加 id
return base.replace(/[^\/]$/, '$&/') + encodeURIComponent(id);
},
// parse 方法存在的意义在于解析远程同步数据时,远端返回的响应对象,
// 该对象默认是 REST API 返回的数据对象,因此可以直接被 set 方法使用。
// 但对于非 REST API 接口响应对象,则需要调用 parse 将其响应结果解析后再返回给 set 使用。
// 所以说,如果直接使用 set 方法设置属性,是无需经过 parse 方法的,只有自动同步远程数据时才需要覆写该方法。
// 例如 fetch 方法中调用了 parse 方法解析远端的响应值,fetch 方法中使用 parse 解析结果去做 set 操作,
// 因此 parse 返回 null 或 undefined 时,set 操作会立即终止。
parse: function(resp, options) {
return resp;
},
// Create a new model with identical attributes to this one.
clone: function() {
return new this.constructor(this.attributes);
},
// A model is new if it has never been saved to the server, and lacks an id.
// 判断一个 model 是否从未保存到远端。
// 判断依据是查看该 model 的 attributes 是否拥有 this.idAttribute 映射字段。
// Backbone.Model 的设计意图是 Model 是远端一条数据的抽象对象(例如数据库中某张表里某一行数据),
// 每个 model 都应该拥有一个主键(对应数据库里数据行的主键值),拥有主键则表示远端已存在该条数据,
// 否则视该 model 为未保存的数据模型。
isNew: function() {
return !this.has(this.idAttribute);
},
// Check if the model is currently in a valid state.
// 检查 model 当前的 attributes 是否处于合法状态(能够通过验证)
isValid: function(options) {
return this._validate({}, _.defaults({validate: true}, options));
},
// Run validation against the next complete set of model attributes,
// returning `true` if all is well. Otherwise, fire an `"invalid"` event.
// 注意:
// validate 是对模拟 set 成功后的 attributes 进行验证,而不仅仅是对 attrs 进行验证。
// 也就是说 this.validate(attrs, options) 中的 attrs 是指模拟 set 成功后的 attributes。
_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;
}
});
// Underscore methods that we want to implement on the Model, mapped to the
// number of arguments they take.
var modelMethods = { keys: 1, values: 1, pairs: 1, invert: 1, pick: 0,
omit: 0, chain: 1, isEmpty: 1 };
// Mix in each Underscore method as a proxy to `Model#attributes`.
addUnderscoreMethods(Model, modelMethods, 'attributes');
// Backbone.Collection
// -------------------
// If models tend to represent a single row of data, a Backbone Collection is
// more analogous to a table full of data ... or a small slice or page of that
// table, or a collection of rows that belong together for a particular reason
// -- all of the messages in this particular folder, all of the documents
// belonging to this particular author, and so on. Collections maintain
// indexes of their models, both in order, and for lookup by `id`.
// Create a new **Collection**, perhaps to contain a specific type of `model`.
// If a `comparator` is specified, the Collection will maintain
// its models in sort order, as they're added and removed.
//
// Collection 构造函数,可以指定 collection 的 model 类型。
// 如果给定 `comparator`,当天新增或移除 model 时,collection 会自动维护 models 的排序。
var Collection = Backbone.Collection = function(models, options) {
options || (options = {});
// 如果 options 中包含 model 字段,则直接绑定到 collection。
if (options.model) this.model = options.model;
// 如果 options 中包含 comparator 且不为 undefined,则直接绑定到 collection。
if (options.comparator !== void 0) this.comparator = options.comparator;
// 重置 collection 的 length, models, _byId 三个属性。
this._reset();
// 初始化 collection
this.initialize.apply(this, arguments);
// 如果指定了 models, 则静默设置初始 models
// todo:
// 与 model 初始化不同,collection 使用 reset 而不是 set 作为构造初始数据的手段,
// 且 reset 操作晚于 initialize 操作。作者意图不是很明确。
// 如此操作的话,则意味着你不应该在 initialize 中对 collection 进行成员增减操作,
// 否则可能会在构造实例时,被构造参数中的 models 覆写了 collection 成员。
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 a = [1,2,3], b = [4,5,6];
// splice(a, b, 1);
// 数组 a 变成 [1, 4, 5, 6, 2, 3]
var splice = function(array, insert, at) {
// 确保 at 是符合 array 长度的合法位置(不小于 0,不大于 array 长度)。
at = Math.min(Math.max(at, 0), array.length);
// 生成切片后半部分等长 Array。
var tail = Array(array.length - at);
// 计算待插入 Array 长度
var length = insert.length;
// 将 array 后半部分成员复制到容器 tail。
for (var i = 0; i < tail.length; i++) tail[i] = array[i + at];
// 将 insert 成员依次插入到 array 的后半部分。
for (i = 0; i < length; i++) array[i + at] = insert[i];
// 将 tail 中成员依次继续插入到 array 尾部。
for (i = 0; i < tail.length; i++) array[i + length + at] = tail[i];
};
// Define the Collection's inheritable methods.
_.extend(Collection.prototype, Events, {
// 默认 model 为 Backbone.Model。
// 大部分情景中你需要重写该属性。
model: Model,
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// 返回一个数组,成员是 collection 中每个 model 的 JSON 值。
toJSON: function(options) {
return this.map(function(model) { return model.toJSON(options); });
},
// Proxy `Backbone.sync` by default.
sync: function() {
return Backbone.sync.apply(this, arguments);
},
// Add a model, or list of models to the set. `models` may be Backbone
// Models or raw JavaScript objects to be converted to Models, or any
// combination of the two.
//
// 使用 set 操作往 collection 添加一个或多个成员。
// models 可以是 Backbone.Model 及其子类实例,或者是纯 Object,或者二者混合组成的数组。
// 关于 options:
// 默认 merge 为 false,但允许指定为 true。
// 强制 add 为 true,remove 为 false,不允许修改。
//
// add 操作的默认行为是在 collection 末尾追加成员,如果成员已经存在,则不追加。
add: function(models, options) {
return this.set(models, _.extend({merge: false}, options, addOptions));
},
// Remove a model, or a list of models from the set.
//
// 从 collection 中移除一个或一组成员。
remove: function(models, options) {
options = _.extend({}, options);
var singular = !_.isArray(models);
models = singular ? [models] : _.clone(models);
// 移除成员
var removed = this._removeModels(models, options);
// 如果 remove 操作非静默,并且的确移除了成员,
// 触发 update 事件。
if (!options.silent && removed) this.trigger('update', this, options);
// 返回被移除的成员(们)。
return singular ? removed[0] : removed;
},
// Update a collection by `set`-ing a new list of models, adding new ones,
// removing models that are no longer present, and merging models that
// already exist in the collection, as necessary. Similar to **Model#set**,
// the core operation for updating the data contained by the collection.
//
// 该方法是 collection 操作 models 的核心方法,重要性等同于 Model#set 方法。
// 该方法用以设置一组新的成员,添加新成员,删除不再具有成员资格的成员,合并已存在的成员。
//
// options:
// add: 如果 model 存在于 models 中但不存在于 collection 中,是否要往 collection 中添加该 model。(默认为真)
// remove: 如果 model 存在于 collection 中但不存在于 models 中,是否要从 collection 中删除该 model。(默认为真)
// merge: 如果 model 存在于 models 中同时也存在于 collection 中,是否要将二者进行合并。(默认为真)
// silent: 是否要触发事件(默认为真)
// sort: 是否要自动排序(默认为真)
// parse: set 操作前是否要经过 parse 方法解析,包括通过纯 Object 生成 Model 实例时,是否要调用 Model.parse 解析(默认为假)
//
// collection#set 操作的本质是,将目标 models 数组中的数据,合并到内部 models 数组中,
// 两个数组的数据合并,涉及到求数据交集、求数据并集、是否合并数据的问题。set 操作就是实现了这三个问题的解决方法。
// 在所有对 models 操作过程中,collection 始终保持对实例 models 的引用一致性(即从来没有更换过 models 数组的指针)
//
// set 操作中,会对根据每个新增的成员和移除的成员依次触发 add 和 remove 事件。
// 所以虽然 set 操作可以通过 options 中 add 和 remove 的值,来实现置换整个 collection.models 内部所有成员,
// 但你的意图是完全置换而不想逐一触发 add 或 remove 事件,那么最好使用 collection#reset 操作,该操作只会触发一个 `reset` 事件。
//
// 如果 models 为 null 或 void 0,会导致 set 操作终止。
// 但如果 parse 方法返回值为 null 或 void 0,或者 parse 方法返回的数组中包含 null 或 void 0,都会被视为一个合法成员,
// collection 会首先寻找该成员是否存在,如果不存在则视为新成员,使用 this.model 来构造新的实例,所以如果该 `成员` 为 null 或 void 0,
// 新实例也会被构造出来并可能被添加到 collection(除非 model 实例在构造时未能通过合法性验证)。
//
// 注意:
// options.parse 对 collection#set 方法有效,而对 model#set 方法无效。
set: function(models, options) {
// 如果 models 为 null 或 undefined,终止 set 操作。
if (models == null) return;
// 准备 options,默认 add: true, remove: true, merge: true。
options = _.defaults({}, options, setOptions);
// 如果 options.parse 为真,且 models 非 Backbone.Model 实例,
// 则调用 this.parse 方法对 models 进行解析。
if (options.parse && !this._isModel(models)) models = this.parse(models, options);
// 如果 models 不是数组,则将其转换为数组。
// 注意:
// 此处操作其实是读取了 models 副本,而非原始 models,
// 以避免后面对 models 的操作会影响到输入的 models。
var singular = !_.isArray(models);
models = singular ? [models] : models.slice();
var at = options.at; // 插入新成员的位置
if (at != null) at = +at; // 将 at 强转为数字类型
if (at < 0) at += this.length + 1; // 如果 at 是负数,则表示位置是倒数的,将其转换为实际位置。
var set = []; // 置换的成员容器
var toAdd = []; // 新添加的成员容器
var toRemove = []; // 待删除的成员容器
var modelMap = {}; // 置换成员映射表
var add = options.add; // 新增标识
var merge = options.merge; // 合并标识
var remove = options.remove; // 移除标识
var sort = false; // 是否需要排序
// 是否具备排序条件(必需定义了 comparator,不能指定插入位置,没有显式声明不排序)
var sortable = this.comparator && (at == null) && options.sort !== false;
var sortAttr = _.isString(this.comparator) ? this.comparator : null; // 如果 comparator 是字符串,则表示使用 model 某个属性作为排序因子
// Turn bare objects into model references, and prevent invalid models
// from being added.
var model;
// 遍历 models,处理那些需要被添加到 collection 的 model
for (var i = 0; i < models.length; i++) {
model = models[i];
// If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
// 查找待添加 model 是否已存在于 collection 中。
var existing = this.get(model);
if (existing) {
// 如果 collection 已保存有目标 model,并且待添加 model 不等于已存在 model。
// 即待添加的是另一个模型实例或者 Object,且 merge 为真,则合并新的 model。
if (merge && model !== existing) {
// 如果待添加 model 为 Backbone.Model 实例,获取它的 attributes 作为 attrs。
var attrs = this._isModel(model) ? model.attributes : model;
// 如果 parse 为真,则需要调用 existing 的 parse 方法来解析 attrs,
// 之后才能对 existing 进行 set 操作。
if (options.parse) attrs = existing.parse(attrs, options);
existing.set(attrs, options);
// 如果具备排序条件,并且没有排序,则重新设定 sort 以标识是否需要排序。
// 如果 existing 中作为排序的因子属性发生了变化,则需要将 sort 设置为真,表示要排序。
if (sortable && !sort) sort = existing.hasChanged(sortAttr);
}
if (!modelMap[existing.cid]) {
modelMap[existing.cid] = true;
set.push(existing);
}
// 将 models 中待添加的 model 替换为 existing。
models[i] = existing;
// If this is a new, valid model, push it to the `toAdd` list.
} else if (add) {
// 如果 options.add 为真,则准备一个待处理的 model。
model = models[i] = this._prepareModel(model, options);
// model 只能是一个 Model 实例或者 false,
// 如果是 false,则表示该 model 不是一个合法的 model,直接忽略。
if (model) {
toAdd.push(model);
// 添加 model 与 collection 之间的引用关系
this._addReference(model, options);
modelMap[model.cid] = true;
set.push(model);
}
}
}
// Remove stale models.
// 如果 options.remove 为真,则需要移除 collection.models 中多余的 model。
if (remove) {
// 遍历 this.models,筛选中待移除的 model,保存在 toRemove 中
for (i = 0; i < this.length; i++) {
model = this.models[i];
// 如果置换成员映射中不包含该 model,则表示它需要被移除。
if (!modelMap[model.cid]) toRemove.push(model);
}
// 移除多余的 model。
if (toRemove.length) this._removeModels(toRemove, options);
}
// See if sorting is needed, update `length` and splice in new models.
// 标识成员的顺序是否发生变化,该标识主要充当是否触发 sort 事件的条件因子。
var orderChanged = false;
// 是否直接置换 collection.models
var replace = !sortable && add && remove;
if (set.length && replace) {
// 如果置换的成员数量与现有成员数量不符,或者任意置换成员与现有成员位置不符,则表示需要重新排序。
orderChanged = this.length != set.length || _.some(this.models, function(model, index) {
return model !== set[index];
});
// 清空现有所有成员
this.models.length = 0;
// 插入置换成员
splice(this.models, set, 0);
// 实时维护 length 属性
this.length = this.models.length;
} else if (toAdd.length) {
// 如果具备排序条件,sort 设置为 true
if (sortable) sort = true;
// 将新的成员插入到 this.models,如果未指定插入位置,则从最末尾插入。
splice(this.models, toAdd, at == null ? this.length : at);
// 实时维护 collection.length 属性
this.length = this.models.length;
}
// Silently sort the collection if appropriate.
// 如果需要排序,则静默排序(阻止排序过程中触发 sort 事件)
if (sort) this.sort({silent: true});
// Unless silenced, it's time to fire all appropriate add/sort events.
// 现在来处理一下事件的事情,
// 如果如非静默操作,需要依次触发可能存在的 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) this.trigger('update', this, options);
}
// Return the added (or merged) model (or models).
// 返回单个 model 或 models
return singular ? models[0] : models;
},
// When you have more items than you want to add or remove individually,
// you can reset the entire set with a new list of models, without firing
// any granular `add` or `remove` events. Fires `reset` when finished.
// Useful for bulk operations and optimizations.
// 使用 reset 操作替代 set 操作来重置整个 collection.models,避免触发 add 或 remove 事件,
// 只有一个 `reset` 事件。
// 注意:
// reset 操作会简单地重新生成一个空数组,并将该数组指针赋值给 collection.models,
// 而之前的 collection.models 会保留在 options.previousModels 作为参数传递给 reset 事件。
//
// reset 与 set 不同之处在于,set 操作维持 collection.models 指针不变,而 reset 会更换 collection.models 指针。
reset: function(models, options) {
options = options ? _.clone(options) : {};
// 遍历现有成员,逐一销毁成员与集合之间的引用关系
for (var i = 0; i < this.models.length; i++) {
this._removeReference(this.models[i], options);
}
// 保留之前的 models 引用
options.previousModels = this.models;
// 重置内部状态(包括更换 this.models)
this._reset();
// 调用 add 操作添加成员(add 操作内部是调用 set 操作)
models = this.add(models, _.extend({silent: true}, options));
// 触发 reset 事件
if (!options.silent) this.trigger('reset', this, options);
return models;
},
// Add a model to the end of the collection.
// 其实 push 等同于 add,你也可以在 options 中指定 at 作为插入位置。
// 而且 model 可以是单个也可以是多个。
push: function(model, options) {
return this.add(model, _.extend({at: this.length}, options));
},
// Remove a model from the end of the collection.
// 移除最后一个成员。
pop: function(options) {
var model = this.at(this.length - 1);
return this.remove(model, options);
},
// Add a model to the beginning of the collection.
// 在内部 models 数组头部追加成员。
// 等同于 add 操作,可以通过 options.at 参数修改 unshift 行为
unshift: function(model, options) {
return this.add(model, _.extend({at: 0}, options));
},
// Remove a model from the beginning of the collection.
// 移除第一个成员
shift: function(options) {
var model = this.at(0);
return this.remove(model, options);
},
// Slice out a sub-array of models from the collection.
// 对 this.models 进行切片操作。
slice: function() {
return slice.apply(this.models, arguments);
},
// Get a model from the set by id.
// 查找成员,obj 可以是一个 id 值,
// 或者是一个包含 collection.model.prototype.idAttribute 属性的对象,
// 或者是一个 model 实例。
// collection 首先尝试使用 id 查找,然后使用 cid 查找。
get: function(obj) {
if (obj == null) return void 0;
var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
},
// Get the model at the given index.
// 获取指定位置的成员,如果 at 为负数,表示倒数位置。
at: function(index) {
if (index < 0) index += this.length;
return this.models[index];
},
// Return models with matching attributes. Useful for simple cases of
// `filter`.
// 查找成员
where: function(attrs, first) {
return this[first ? 'find' : 'filter'](attrs);
},
// Return the first model with matching attributes. Useful for simple cases
// of `find`.
// 查找成员
findWhere: function(attrs) {
return this.where(attrs, true);
},
// Force the collection to re-sort itself. You don't need to call this under
// normal circumstances, as the set will maintain sort order as each item
// is added.
//
// 强制对 collection 成员进行排序,但如果没有声明 comparator,则抛出异常。
sort: function(options) {
var comparator = this.comparator;
if (!comparator) throw new Error('Cannot sort a set without a comparator');
options || (options = {});
// length 变量记录 comparator 长度,主要意图是记录 comparator 作为函数时,期待参数的个数。
var length = comparator.length;
if (_.isFunction(comparator)) comparator = _.bind(comparator, this);
// Run sort based on type of `comparator`.
// 如果 comparator 是一个接受单个参数的函数,或者字符串,
// 则使用 sortBy 进行(升序)排序,否则对 models 进行原生数组排序。
if (length === 1 || _.isString(comparator)) {
this.models = this.sortBy(comparator);
} else {
this.models.sort(comparator);
}
// 如果 sort 为非静默操作,则触发 sort 事件
if (!options.silent) this.trigger('sort', this, options);
return this;
},
// Pluck an attribute from each model in the collection.
// 获取每个成员指定的 attribute。
// 注意:这里使用 get 方法获取属性,而不是直接使用 _.pluck(this.toJSON(), attr);
// 这样做避免了直接读取 model.attributes,如果 model 的 get 方法被改写了,也可以正确返回相应的值。
pluck: function(attr) {
return _.invoke(this.models, 'get', attr);
},
// Fetch the default set of models for this collection, resetting the
// collection when they arrive. If `reset: true` is passed, the response
// data will be passed through the `reset` method instead of `set`.
//
// 与 Model#fetch 方法类似,如果 options.reset 为真,则使用 collection.reset 处理远端响应,
// 否则使用 collection.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);
},
// Create a new instance of a model in this collection. Add the model to the
// collection immediately, unless `wait: true` is passed, in which case we
// wait for the server to agree.
// 通过 Model#save 方法实现的创建 model。
create: function(model, options) {
options = options ? _.clone(options) : {};
var wait = options.wait;
// 准备 model,如果准备 model 失败,则直接终止 create 操作,并返回 false。
model = this._prepareModel(model, options);
if (!model) return false;
// 如果不等待服务器响应,则直接添加 model 到 collection.models。
// 这意味着,无论 model 是否 validate 与否,它都会被添加到 collection 中。
// 因为 model 实例化过程中,无论 validate 成功失败,都不能阻止 model 构造完成。
// 而等待服务器响应,在 model.save 过程中,可以对 attributes 进行合法性验证,
// 从而阻止 options.success 被调用,也就阻止了非法的 model 被添加到 collection 中。
if (!wait) this.add(model, options);
// 否则等到服务器响应成功后再将 model 添加到 collection
var collection = this;
var success = options.success;
options.success = function(model, resp, callbackOpts) {
if (wait) collection.add(model, callbackOpts);
if (success) success.call(callbackOpts.context, model, resp, callbackOpts);
};
// 通过 model.save 方法实现 create,即 colleciton 本身是不负责真正的 model 数据同步。
model.save(null, options);
return model;
},
// **parse** converts a response into a list of models to be added to the
// collection. The default implementation is just to pass it through.
//
// 本方法将远端响应转换为一个列表(待添加成员列表)或者是一个成员对象。
// parse 返回任何对象(包括 null, undefined),如果不是数组,都会被转换为数组,
// 然后被 collection 用作查找已存在成员的因子,或者作为 this.model 构造函数的 attributes 参数。
parse: function(resp, options) {
return resp;
},
// Create a new collection with an identical list of models as this one.
clone: function() {
return new this.constructor(this.models, {
model: this.model,
comparator: this.comparator
});
},
// Define how to uniquely identify models in the collection.
//
// 该方法主要用于让 collection 给每个成员生成一个唯一标识。
// collection 内部需要判定成员身份的操作都需要调用该方法。
modelId: function (attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
},
// 私有方法,重置 collection 内部状态(主要是 collection 的 length, models, _byId)。
// 只有在 collection 进行初始化或 reset 操作时才调用该方法。
_reset: function() {
// Collection 是实时维护 length 属性,
// 而不是通过 this.models.length 求值来获取成员长度。
this.length = 0;
this.models = [];
this._byId = {};
},
// Prepare a hash of attributes (or other model) to be added to this
// collection.
//
//
_prepareModel: function(attrs, options) {
// 如果 attrs 是一个 Backbone.Model 实例,且该模型未属于其他 collection,则为其添加 collection 属性。
// 这意味着一个 model 不能同时关联到两个 collection。
if (this._isModel(attrs)) {
if (!attrs.collection) attrs.collection = this;
return attrs;
}
// 如果 attrs 不是 Backbone.Model 实例,
// 则使用 this.model 作为构造函数,构造一个 Backbone.Model 实例。
options = options ? _.clone(options) : {};
options.collection = this;
var model = new this.model(attrs, options);
// 如果构造 model 实例过程中,没有发生数据验证失败,
// 则表示新构造的 model 是一个合法的 model,直接返回该 model。
// 否则在 collection 触发 invalid 事件,并返回 false。
if (!model.validationError) return model;
this.trigger('invalid', this, model.validationError, options);
return false;
},
// Internal method called by both remove and set.
//
// 私有方法,在 remove 和 set 操作中调用,用以移除成员。
_removeModels: function(models, options) {
// 回收被移除成员的容器
var removed = [];
// 遍历移除条件对象 models
for (var i = 0; i < models.length; i++) {
// 查找待移除成员
var model = this.get(models[i]);
// 如未找到则进行下一轮循环
if (!model) continue;
// 查找待移除成员的位置,并从 this.models 中将其移除
var index = this.indexOf(model);
this.models.splice(index, 1);
// 将 collection 的 length 属性减一
this.length--;
// 如果 remove 操作非静默,则触发 remove 事件。
// 在 options 中记录被移除成员的位置。
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
// 在回收容器中保存被移除成员
removed.push(model);
// 销毁被移除成员与 collection 之间的引用关系
this._removeReference(model, options);
}
// 返回 false 表示没有任何成员被移除,否则返回所有被移除成员的集合
return removed.length ? removed : false;
},
// Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
//
// 私有方法,检查 model 是否是 Backbone.Model 实例
_isModel: function (model) {
return model instanceof Model;
},
// Internal method to create a model's ties to a collection.
// 私有方法,添加成员与集合之间的引用关系。
_addReference: function(model, options) {
// 在 this._byId 中添加成员映射关系
// 首先使用成员的 cid 添加映射,
// 然后通过 modelId 对成员求值,添加映射关系。
// 也就是说,通常 collection 会保存对成员的两个引用关系,
// 一个是通过 cid,另一个是通过 idAttribute。
this._byId[model.cid] = model;
var id = this.modelId(model.attributes);
if (id != null) this._byId[id] = model;
// 为 model 添加 all 事件回调。
// 这里是通过成员的 on 方法添加回调,而不是 listenTo 成员,
// 因此如果成员执行 model.off('all'),那么成员的任何事件都不会再转发到 collection。
// 很难说此处使用 listenTo 或 on 的优劣,但使用 on,则将事件主动权交到了成员手中。
model.on('all', this._onModelEvent, this);
},
// Internal method to sever a model's ties to a collection.
//
// 私有方法,用以销毁成员与集合之间的引用关系。
_removeReference: function(model, options) {
// 依次删除使用 cid 与 idAttribute 对成员进行引用的关系
delete this._byId[model.cid];
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
// 销毁 model 的 collection 属性
if (this === model.collection) delete model.collection;
// 从 model 的 all 事件回调队列中,移除与本 collection 相关的回调函数。
model.off('all', this._onModelEvent, this);
},
// Internal method called every time a model in the set fires an event.
// Sets need to update their indexes when models change ids. All other
// events simply proxy through. "add" and "remove" events that originate
// in other collections are ignored.
//
// 响应成员的 all 事件
_onModelEvent: function(event, model, collection, options) {
// add 和 remove 是来自 collection 自身,因此不再转发该两个事件。
if ((event === 'add' || event === 'remove') && collection !== this) return;
// 当成员发生 destroy 事件时,从 collection 移除该成员。
if (event === 'destroy') this.remove(model, options);
if (event === 'change') {
// 当成员发生 change 事件时,意味着成员的 id 属性可能发生变化,
// 所以需要在 collection 中重新检视成员的 idAttribute 引用关系。
var prevId = this.modelId(model.previousAttributes());
var id = this.modelId(model.attributes);
if (prevId !== id) {
if (prevId != null) delete this._byId[prevId];
if (id != null) this._byId[id] = model;
}
}
// 转发成员事件
this.trigger.apply(this, arguments);
}
});
// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var collectionMethods = { forEach: 3, each: 3, map: 3, collect: 3, reduce: 4,
foldl: 4, inject: 4, reduceRight: 4, foldr: 4, 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};
// Mix in each Underscore method as a proxy to `Collection#models`.
addUnderscoreMethods(Collection, collectionMethods, 'models');
// Backbone.View
// -------------
// Backbone Views are almost more convention than they are actual code. A View
// is simply a JavaScript object that represents a logical chunk of UI in the
// DOM. This might be a single item, an entire list, a sidebar or panel, or
// even the surrounding frame which wraps your whole app. Defining a chunk of
// UI as a **View** allows you to define your DOM events declaratively, without
// having to worry about render order ... and makes it easy for the view to
// react to specific changes in the state of your models.
// Creating a Backbone.View creates its initial element outside of the DOM,
// if an existing element is not provided...
//
// 视图构造函数
var View = Backbone.View = function(options) {
// 生成唯一标识
this.cid = _.uniqueId('view');
// 绑定实例属性
_.extend(this, _.pick(options, viewOptions));
// 创建根节点
this._ensureElement();
this.initialize.apply(this, arguments);
};
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/;
// List of view options to be set as properties.
var viewOptions = ['model', 'collection', 'el', 'id', 'attributes', 'className', 'tagName', 'events'];
// Set up all inheritable **Backbone.View** properties and methods.
_.extend(View.prototype, Events, {
// The default `tagName` of a View's element is `"div"`.
tagName: 'div',
// jQuery delegate for element lookup, scoped to DOM elements within the
// current view. This should be preferred to global lookups where possible.
// 查询本视图作用域中的元素
$: function(selector) {
return this.$el.find(selector);
},
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// **render** is the core function that your view should override, in order
// to populate its element (`this.el`), with the appropriate HTML. The
// convention is for **render** to always return `this`.
render: function() {
return this;
},
// Remove this view by taking the element out of the DOM, and removing any
// applicable Backbone.Events listeners.
// 移除根节点,销毁所有监听事件。
remove: function() {
this._removeElement();
this.stopListening();
return this;
},
// Remove this view's element from the document and all event listeners
// attached to it. Exposed for subclasses using an alternative DOM
// manipulation API.
// 私有方法,移除根节点。
_removeElement: function() {
this.$el.remove();
},
// Change the view's element (`this.el` property) and re-delegate the
// view's events on the new element.
// 设置根节点元素。包括解绑之前节点委托事件,更换根节点,重新委托事件。
setElement: function(element) {
this.undelegateEvents();
this._setElement(element);
this.delegateEvents();
return this;
},
// Creates the `this.el` and `this.$el` references for this view using the
// given `el`. `el` can be a CSS selector or an HTML string, a jQuery
// context or an element. Subclasses can override this to utilize an
// alternative DOM manipulation API and are only required to set the
// `this.el` property.
// 私有方法,设置根节点。
// 参数 el 可以是一个 DocumentElement,或者是一个 jQuery 实例。
_setElement: function(el) {
this.$el = el instanceof Backbone.$ ? el : Backbone.$(el);
this.el = this.$el[0];
},
// Set callbacks, where `this.events` is a hash of
//
// *{"event selector": "callback"}*
//
// {
// 'mousedown .title': 'edit',
// 'click .button': 'save',
// 'click .open': function(e) { ... }
// }
//
// pairs. Callbacks will be bound to the view, with `this` set properly.
// Uses event delegation for efficiency.
// Omitting the selector binds the event to `this.el`.
// 委托根节点事件,缺省使用 this.events 作为委托事件。
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], _.bind(method, this));
}
return this;
},
// Add a single event listener to the view's element (or a child element
// using `selector`). This only works for delegate-able events: not `focus`,
// `blur`, and not `change`, `submit`, and `reset` in Internet Explorer.
// 委托事件给视图根节点
delegate: function(eventName, selector, listener) {
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
// Clears all callbacks previously bound to the view by `delegateEvents`.
// You usually don't need to use this, but may wish to if you have multiple
// Backbone views attached to the same DOM element.
// 清空根节点所有委托事件
undelegateEvents: function() {
if (this.$el) this.$el.off('.delegateEvents' + this.cid);
return this;
},
// A finer-grained `undelegateEvents` for removing a single delegated event.
// `selector` and `listener` are both optional.
// 利用 jQuery 清除委托事件
undelegate: function(eventName, selector, listener) {
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
return this;
},
// Produces a DOM element to be assigned to your view. Exposed for
// subclasses using an alternative DOM manipulation API.
// 创建 DOM 元素(作为根节点使用)
_createElement: function(tagName) {
return document.createElement(tagName);
},
// Ensure that the View has a DOM element to render into.
// If `this.el` is a string, pass it through `$()`, take the first
// matching element, and re-assign it to `el`. Otherwise, create
// an element from the `id`, `className` and `tagName` properties.
//
// 创建根节点。
_ensureElement: function() {
// 如果没有给定根节点,则自动生成一个根节点。
if (!this.el) {
var attrs = _.extend({}, _.result(this, 'attributes'));
// 添加根节点 ID
if (this.id) attrs.id = _.result(this, 'id');
// 添加根节点类
if (this.className) attrs['class'] = _.result(this, 'className');
// 创建视图根节点
this.setElement(this._createElement(_.result(this, 'tagName')));
// 设置根节点 CSS 属性
this._setAttributes(attrs);
} else {
// 使用给定的根节点(Element 或 jQuery 实例)创建视图根节点。
this.setElement(_.result(this, 'el'));
}
},
// Set attributes from a hash on this view's element. Exposed for
// subclasses using an alternative DOM manipulation API.
// 设置根节点 CSS 属性
_setAttributes: function(attributes) {
this.$el.attr(attributes);
}
});
// Backbone.sync
// -------------
// Override this function to change the manner in which Backbone persists
// models to the server. You will be passed the type of request, and the
// model in question. By default, makes a RESTful Ajax request
// to the model's `url()`. Some possible customizations could be:
//
// * Use `setTimeout` to batch rapid-fire updates into a single request.
// * Send up the models as XML instead of JSON.
// * Persist models via WebSockets instead of Ajax.
//
// Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests
// as `POST`, with a `_method` parameter containing the true HTTP method,
// as well as all requests with the body as `application/x-www-form-urlencoded`
// instead of `application/json` with the model in a param named `model`.
// Useful when interfacing with server-side languages like **PHP** that make
// it difficult to read the body of `PUT` requests.
//
// 如果启用 `Backbone.emulatedHTTP` ,那么 Backbone 会将 `PUT` 和 `DELETE` 请求改为 `POST` 请求,
// 同时增加一个 `_method` 参数用以记录原本的请求方法。
Backbone.sync = function(method, model, options) {
// sync 函数参数 method 取值范围为:create, read, update, delete, patch;
// 分别映射到 HTTP 请求方法:POST, GET, PUT, DELETE, PATCH
// 这里是将 sync 的 method 转换为 HTTP 请求方法名。
var type = methodMap[method];
// Default options, unless specified.
_.defaults(options || (options = {}), {
emulateHTTP: Backbone.emulateHTTP,
emulateJSON: Backbone.emulateJSON
});
// 默认请求 JSON 数据。
// 局部变量 params 表示最后 ajax 请求参数
var params = {type: type, dataType: 'json'};
// 检查是否输入 URL 或者 model 是否自带 URL
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
// Ensure that we have the appropriate request data.
// 如果 options 未给定 data 字段(即 model.fetch(options) 中的 options)
// 并且同步的方法是写操作,那么默认的 xhr 请求中 contentType 应为 json。
// 提交的 data 优先从 options.attrs 读取,其次读取 model.toJSON()。
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
// 如果设置了 Backbone.emulateJSON 为真,则使用 application/x-www-form-urlencoded 格式提交数据。
// 注意:
// 这里并不是将 data 直接编码成 HTML-form 格式,而是将整个 data 封装在 model 字段中提交。
// 如果不这样做,当 model 为 collection 时,实际的 data 是一个数组,不适宜作为 form 提交。
if (options.emulateJSON) {
params.contentType = 'application/x-www-form-urlencoded';
params.data = params.data ? {model: params.data} : {};
}
// For older servers, emulate HTTP by mimicking the HTTP method with `_method`
// And an `X-HTTP-Method-Override` header.
// 如果设置 Backbone.emulateHTTP 为真,且 sync 为写操作,
// 则统一使用 POST 方法请求,并且将原始请求方法保存在 data._method 字段中。
// 同时增加 xhr 请求头 X-HTTP-Method-Override。
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);
};
}
// Don't process data on a non-GET request.
// jQeury 的 ajax 方法,如果提交的 data 为非字符串对象,会被默认转换为 query string。
// 以匹配默认的 application/x-www-form-urlencode 类型文档。
// 因此对于非 GET 且未要求 emulateJSON 的请求,设置 processData 为否以阻止 jQuery 这一默认行为。
if (params.type !== 'GET' && !options.emulateJSON) {
params.processData = false;
}
// 重新封装 options 中的 error,将 textStatus 和 errorThrown 记录到 options 中。
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);
};
// 使用 Backbone.ajax 发起 xhr 请求,并且将 xhr 保存在 options 中。
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
// 发起请求后立即出发 request 事件,告知第三方已发起了一次 xhr 请求。
model.trigger('request', model, xhr, options);
return xhr;
};
// 默认的 Backbone.sync 中 method 与 http 请求的映射关系。
// 为什么要自定义一套 Backbone.sync 的方法名而不直接使用 HTTP 请求方法名?
// 因为这样可以将 Backbone 的同步操作与 HTTP 请求分离开,
// 因为你也可以通过其他渠道来实现数据同步,例如通过改写 sync 方法来与 local storage 同步数据。
var methodMap = {
'create': 'POST',
'update': 'PUT',
'patch': 'PATCH',
'delete': 'DELETE',
'read': 'GET'
};
// Set the default implementation of `Backbone.ajax` to proxy through to `$`.
// Override this if you'd like to use a different library.
// 默认使用 jQuery.ajax 方法实现数据同步,如果使用其他库,可以改写此同步方法。
Backbone.ajax = function() {
return Backbone.$.ajax.apply(Backbone.$, arguments);
};
// Backbone.Router - 路由
// -------------------------
// Routers map faux-URLs to actions, and fire events when routes are
// matched. Creating a new one sets its `routes` hash, if not set statically.
//
// Router 构造函数
var Router = Backbone.Router = function(options) {
options || (options = {});
if (options.routes) this.routes = options.routes;
this._bindRoutes();
this.initialize.apply(this, arguments);
};
// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam = /(\(\?)?:\w+/g;
var splatParam = /\*\w+/g;
var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;
// Set up all inheritable **Backbone.Router** properties and methods.
_.extend(Router.prototype, Events, {
// Initialize is an empty function by default. Override it with your own
// initialization logic.
initialize: function(){},
// Manually bind a single named route to a callback. For example:
//
// this.route('search/:query/p:num', 'search', function(query, num) {
// ...
// });
//
// 手动添加路由
// @param route: 字符串或正则表达式,表示路由路径。
// @param name: 路由名称,表示路由器处理路由的方法(this[name]),或者 name 就是响应函数(相当于 callback)。
// @param callback: 如果没有给定 callback,则使用 this[name],否则使用 callback 作为路由响应函数。
route: function(route, name, callback) {
// 如果 route 不是正则表达式,则将其转换为正则表达式。
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
if (_.isFunction(name)) {
callback = name;
name = '';
}
if (!callback) callback = this[name];
var router = this;
// 在 Backbone.history 中添加路由(正则表达式)
Backbone.history.route(route, function(fragment) {
var args = router._extractParameters(route, fragment);
// 如果 router.execute 方法返回 false,则不触发任何事件。
// 默认 router.execute 方法返回值固定为 void 0,因此一定会触发事件。
// 如果要阻止触发事件,只能是重写 router.execute 方法。
if (router.execute(callback, args, name) !== false) {
// 是的,如果 route 第二个参数为函数,那么 name 就是空字符串。
// 因此触发的事件是 'route:'。
// router 触发了两个看似相同的事件,一个是 `route:name`,另一个是 `router`。
router.trigger.apply(router, ['route:' + name].concat(args));
router.trigger('route', name, args);
Backbone.history.trigger('route', router, name, args);
}
});
return this;
},
// Execute a route handler with the provided parameters. This is an
// excellent place to do pre-route setup or post-route cleanup.
// 执行路由回调函数。
execute: function(callback, args, name) {
if (callback) callback.apply(this, args);
},
// Simple proxy to `Backbone.history` to save a fragment into the history.
navigate: function(fragment, options) {
Backbone.history.navigate(fragment, options);
return this;
},
// Bind all defined routes to `Backbone.history`. We have to reverse the
// order of the routes here to support behavior where the most general
// routes can be defined at the bottom of the route map.
// 将所有路由绑定到 `Backbone.history`。
_bindRoutes: function() {
// 如果未定义路由,则终止绑定操作。
if (!this.routes) return;
// 对 this.routes 求值。
this.routes = _.result(this, 'routes');
var route, routes = _.keys(this.routes);
// 遍历 this.routes,逐一添加路由
while ((route = routes.pop()) != null) {
this.route(route, this.routes[route]);
}
},
// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_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]*))?$');
},
// Given a route, and a URL fragment that it matches, return the array of
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
//
// 从路由路径中提取参数。
// @param route: 路由正则表达式
// @param fragment: 被 Backbone.History 确认匹配的 URL 路径。
_extractParameters: function(route, fragment) {
var params = route.exec(fragment).slice(1);
return _.map(params, function(param, i) {
// Don't decode the search params.
if (i === params.length - 1) return param || null;
return param ? decodeURIComponent(param) : null;
});
}
});
// Backbone.History
// ----------------
// Handles cross-browser history management, based on either
// [pushState](http://diveintohtml5.info/history.html) and real URLs, or
// [onhashchange](https://developer.mozilla.org/en-US/docs/DOM/window.onhashchange)
// and URL fragments. If the browser supports neither (old IE, natch),
// falls back to polling.
//
// 使用 HTML5 History API 或者 onhashchange 事件实现历史记录操作。
var History = Backbone.History = function() {
this.handlers = [];
this.checkUrl = _.bind(this.checkUrl, this);
// Ensure that `History` can be used outside of the browser.
if (typeof window !== 'undefined') {
this.location = window.location;
this.history = window.history;
}
};
// Cached regex for stripping a leading hash/slash and trailing space.
// 正则表达式:用以删除字符串头部的 `#` 或 `/` 字符,以及尾部的空白。
// 例如:'/abc/ '.replace(routeStripper, '') 得到 'abc/'
var routeStripper = /^[#\/]|\s+$/g;
// Cached regex for stripping leading and trailing slashes.
// 正则表达式:删除字符串头尾的 `/` 字符(确保字符串不以 `/` 开头或结尾)
var rootStripper = /^\/+|\/+$/g;
// Cached regex for stripping urls of hash.
// 正则表达式:删除字符串中 '#' 字符(包含井字符)后所有字符。
var pathStripper = /#.*$/;
// Has the history handling already been started?
History.started = false;
// Set up all inheritable **Backbone.History** properties and methods.
_.extend(History.prototype, Events, {
// The default interval to poll for hash changes, if necessary, is
// twenty times a second.
interval: 50,
// Are we at the app root?
atRoot: function() {
var path = this.location.pathname.replace(/[^\/]$/, '$&/');
return path === this.root && !this.getSearch();
},
// Does the pathname match the root?
matchRoot: function() {
var path = this.decodeFragment(this.location.pathname);
var root = path.slice(0, this.root.length - 1) + '/';
return root === this.root;
},
// Unicode characters in `location.pathname` are percent encoded so they're
// decoded for comparison. `%25` should not be decoded since it may be part
// of an encoded parameter.
// 将 fragment 从百分号编码解码成 UNICODE,但不解码 `%25`,因为它有可能是被编码的参数。
decodeFragment: function(fragment) {
return decodeURI(fragment.replace(/%25/g, '%2525'));
},
// In IE6, the hash fragment and search params are incorrect if the
// fragment contains `?`.
getSearch: function() {
var match = this.location.href.replace(/#.*/, '').match(/\?.+/);
return match ? match[0] : '';
},
// Gets the true hash value. Cannot use location.hash directly due to bug
// in Firefox where location.hash will always be decoded.
getHash: function(window) {
var match = (window || this).location.href.match(/#(.*)$/);
return match ? match[1] : '';
},
// Get the pathname and search params, without the root.
getPath: function() {
var path = this.decodeFragment(
this.location.pathname + this.getSearch()
).slice(this.root.length - 1);
return path.charAt(0) === '/' ? path.slice(1) : path;
},
// Get the cross-browser normalized URL fragment from the path or hash.
getFragment: function(fragment) {
if (fragment == null) {
if (this._usePushState || !this._wantsHashChange) {
fragment = this.getPath();
} else {
fragment = this.getHash();
}
}
return fragment.replace(routeStripper, '');
},
// Start the hash change handling, returning `true` if the current URL matches
// an existing route, and `false` otherwise.
// 启动 History 路由,如果当前 URL 匹配到了某条路由,返回 true,否则返回 false。
start: function(options) {
// History 是个单例应用,不允许重复启动。
if (History.started) throw new Error('Backbone.history has already been started');
History.started = true;
// Figure out the initial configuration. Do we need an iframe?
// Is pushState desired ... is it available?
this.options = _.extend({root: '/'}, this.options, options);
this.root = this.options.root;
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();
// Normalize root to always include a leading and trailing slash.
this.root = ('/' + this.root + '/').replace(rootStripper, '/');
// Transition from hashChange to pushState or vice versa if both are
// requested.
if (this._wantsHashChange && this._wantsPushState) {
// If we've started off with a route from a `pushState`-enabled
// browser, but we're currently in a browser that doesn't support it...
if (!this._hasPushState && !this.atRoot()) {
var root = this.root.slice(0, -1) || '/';
this.location.replace(root + '#' + this.getPath());
// Return immediately as browser will do redirect to new url
return true;
// Or if we've started out with a hash-based route, but we're currently
// in a browser where it could be `pushState`-based instead...
} else if (this._hasPushState && this.atRoot()) {
this.navigate(this.getHash(), {replace: true});
}
}
// Proxy an iframe to handle location events if the browser doesn't
// support the `hashchange` event, HTML5 history, or the user wants
// `hashChange` but not `pushState`.
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;
// Using `appendChild` will throw on IE < 9 if the document is not ready.
var iWindow = body.insertBefore(this.iframe, body.firstChild).contentWindow;
iWindow.document.open();
iWindow.document.close();
iWindow.location.hash = '#' + this.fragment;
}
// Add a cross-platform `addEventListener` shim for older browsers.
var addEventListener = window.addEventListener || function (eventName, listener) {
return attachEvent('on' + eventName, listener);
};
// Depending on whether we're using pushState or hashes, and whether
// 'onhashchange' is supported, determine how we check the URL state.
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();
},
// Disable Backbone.history, perhaps temporarily. Not useful in a real app,
// but possibly useful for unit testing Routers.
stop: function() {
// Add a cross-platform `removeEventListener` shim for older browsers.
var removeEventListener = window.removeEventListener || function (eventName, listener) {
return detachEvent('on' + eventName, listener);
};
// Remove window listeners.
if (this._usePushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._useHashChange && !this.iframe) {
removeEventListener('hashchange', this.checkUrl, false);
}
// Clean up the iframe if necessary.
if (this.iframe) {
document.body.removeChild(this.iframe);
this.iframe = null;
}
// Some environments will throw when clearing an undefined interval.
if (this._checkUrlInterval) clearInterval(this._checkUrlInterval);
History.started = false;
},
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
this.handlers.unshift({route: route, callback: callback});
},
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
var current = this.getFragment();
// If the user pressed the back button, the iframe's hash will have
// changed and we should use that for comparison.
if (current === this.fragment && this.iframe) {
current = this.getHash(this.iframe.contentWindow);
}
if (current === this.fragment) return false;
if (this.iframe) this.navigate(current);
this.loadUrl();
},
// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragment) {
// If the root doesn't match, no routes can match either.
if (!this.matchRoot()) return false;
fragment = this.fragment = this.getFragment(fragment);
return _.some(this.handlers, function(handler) {
if (handler.route.test(fragment)) {
handler.callback(fragment);
return true;
}
});
},
// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
//
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
if (!History.started) return false;
if (!options || options === true) options = {trigger: !!options};
// Normalize the fragment.
fragment = this.getFragment(fragment || '');
// Don't include a trailing slash on the root.
var root = this.root;
if (fragment === '' || fragment.charAt(0) === '?') {
root = root.slice(0, -1) || '/';
}
var url = root + fragment;
// Strip the hash and decode for matching.
fragment = this.decodeFragment(fragment.replace(pathStripper, ''));
if (this.fragment === fragment) return;
this.fragment = fragment;
// If pushState is available, we use it to set the fragment as a real URL.
if (this._usePushState) {
this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
// If hash changes haven't been explicitly disabled, update the hash
// fragment to store history.
} 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;
// Opening and closing the iframe tricks IE7 and earlier to push a
// history entry on hash-tag change. When replace is true, we don't
// want this.
if (!options.replace) {
iWindow.document.open();
iWindow.document.close();
}
this._updateHash(iWindow.location, fragment, options.replace);
}
// If you've told us that you explicitly don't want fallback hashchange-
// based history, then `navigate` becomes a page refresh.
} else {
return this.location.assign(url);
}
if (options.trigger) return this.loadUrl(fragment);
},
// Update the hash location, either replacing the current entry, or adding
// a new one to the browser history.
_updateHash: function(location, fragment, replace) {
if (replace) {
var href = location.href.replace(/(javascript:|#).*$/, '');
location.replace(href + '#' + fragment);
} else {
// Some browsers require that `hash` contains a leading #.
location.hash = '#' + fragment;
}
}
});
// Create the default Backbone.history.
Backbone.history = new History;
// Helpers
// -------
// Helper function to correctly set up the prototype chain for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
// `extend` 函数通过设置子类的原型链实现继承机制,它可以同时扩展父类的原型
// 属性和类属性。
var extend = function(protoProps, staticProps) {
var parent = this; // 上下文应指向父类
var child; // 子类(构造函数)
// 当传入原型对象包含 `constructor` 属性,则直接作为子类的构造函数。
// 否则新建一个构造函数,并在构造函数中调用父类构造函数。
if (protoProps && _.has(protoProps, 'constructor')) {
child = protoProps.constructor;
} else {
child = function(){ return parent.apply(this, arguments); };
}
// 将父类静态属性和新传入的静态属性扩展到子类上。
_.extend(child, parent, staticProps);
// 设置中间人(或者代理构造函数),将中间人 constructor 属性设置为子类构造函数。
// 将子类的 prototype 设置为中间人实例,从而使得子类处于中间人原型链上。
// 避免将子类 prototype 直接指向中间人的 prototype,可以使得对父类 prototype 的修改,
// 直接作用到子类上,但对子类 prototype 的修改,会被父类实例隔绝,从而避免作用到父类身上。
//
// 使用中间人连接 child 和 parent,不将 child 的 prototype 直接指向 parent 的 prototype。
// 原因在于子类 prototype 应指向父类实例,从而避免原型链上的逆向作用。
// 使用中间人,将中间人的 prototype 指向 parent 的 prototype,
// 可以保证实现继承同时避免调用 parent 的构造函数,从而带来副作用。
var Surrogate = function(){ this.constructor = child; };
Surrogate.prototype = parent.prototype;
child.prototype = new Surrogate;
// 扩展子类的原型(实例方法)
if (protoProps) _.extend(child.prototype, protoProps);
// 添加 __super__ 属性指向父类的原型,以便在子类中可以调用父类原型。
child.__super__ = parent.prototype;
return child;
};
// Set up inheritance for the model, collection, router, view and history.
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
// Throw an error when a URL is needed, and none is supplied.
// 异常:URL 不存在
var urlError = function() {
throw new Error('A "url" property or function must be specified');
};
// Wrap an optional error callback with a fallback error event.
// 封装 error 回调(在 model.fetch 方法中,ajax 的 error 回调)
// 保证无论是否存在 options.error 回调,都会触发 model 的 error 事件。
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);
};
};
return Backbone;
}));