Last active
September 15, 2017 22:01
-
-
Save isayme/3f40a273c03a4d939768 to your computer and use it in GitHub Desktop.
Backbone源码注释
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Backbone.js 1.1.2 | |
// (c) 2010-2014 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors | |
// Backbone may be freely distributed under the MIT license. | |
// For all details and documentation: | |
// http://backbonejs.org | |
// 典型自执行函数格式: | |
// (funtion(root, factory) { | |
// // do stuff here | |
// })(this, factoryFunc); | |
// 其中`root`就是`this`, `factory`就是`factoryFunc`; | |
// 而`this`又根据环境的不同而不同, 具体见下面的注释. | |
// `factory`的入参格式:function(root, Backbone, _, $); 返回值为修改之后的入参`Backbone`. | |
// 其中`root`就是上面自执行函数中的入参`root`; | |
// `Backbone` | |
(function(root, factory) { | |
// Set up Backbone appropriately for the environment. Start with AMD. | |
// 如果是AMD的模块规范(require.js使用的规范, 主要用于浏览器端), 则定义Backbone为AMD格式 | |
// AMD规范中, define是定义模块的函数, 且define.amd确保这是AMD规范的模块. | |
if (typeof define === 'function' && define.amd) { | |
// 使用define函数定义Backbone模块, 依赖`underscore`, `jquery`, `exports`三个模块. | |
// 此时的`root`是`exports`, 所以`root.Backbone`就等价于`exports.Backbone`. | |
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. | |
// 如果是CommonJS的模块规范(NodeJS使用的规范, 主要用于服务器端, 所以jQuery非必须). | |
// CommonJS规范中, exports是用于导出模块的对象. | |
} else if (typeof exports !== 'undefined') { | |
// 导入`underscore`库. | |
var _ = require('underscore'); | |
// 注意factory的格式是: `function(root, Backbone, _, $)` | |
// 此时的`root`是`exports`, 对`root`的所有更改其实都作用于`exports`. | |
// 第二个参数`Backbone`传入的是`exports`, 所以factory内部对`Backbone`的所有更改其实都作用于`exports`. | |
// 第三个参数是underscore. | |
// 服务器端不需要jQuery, 所以第四个参数为空. | |
factory(root, exports, _); | |
// Finally, as a browser global. | |
// 如果没有使用任何模块加载方案, 即通常的浏览器<script>加载方式. | |
} else { | |
// 浏览器中, root指的是`windows`对象. | |
// 注意factory的格式是: `function(root, Backbone, _, $)` | |
// 第一个参数`root`, 对`root`的所有更改其实都作用于`windows`, | |
// 其实在factory内部`root`的主要用于noConflict函数, 作用是保存之前的已经存在的root.Backbone变量, 防止覆盖. | |
// Backbone的所有属性都添加到第二个参数`{}`中, 函数返回修改之后的`{}`(即Backbone)并赋值给root.Backbone; | |
// 即这时候就存在windows.Backbone了. | |
// 第三个参数是传入underscore, 所以必须保证Backbone载入前underscore已经载入. | |
// 第四个参数是jQuery或类似的DOM操作库. | |
root.Backbone = factory(root, {}, root._, (root.jQuery || root.Zepto || root.ender || root.$)); | |
} | |
}(this, 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. | |
// 保存之前的`Backbone`变量, 防止命名冲突; 与之后的`noConflict`函数结合使用. | |
var previousBackbone = root.Backbone; | |
// Create local references to array methods we'll want to use later. | |
// 创建数组方法`push`, `slice`, `splice`的引用, 之后会经常用到. | |
var array = []; | |
var push = array.push; | |
var slice = array.slice; | |
var splice = array.splice; | |
// Current version of the library. Keep in sync with `package.json`. | |
// 当前版本号是`1.1.2`. | |
Backbone.VERSION = '1.1.2'; | |
// For Backbone's purposes, jQuery, Zepto, Ender, or My Library (kidding) owns | |
// the `$` variable. | |
// 保存jQuery(或者类似的Zepto等), Backbone中的DOM操作将用到. | |
Backbone.$ = $; | |
// Runs Backbone.js in *noConflict* mode, returning the `Backbone` variable | |
// to its previous owner. Returns a reference to this Backbone object. | |
// 提供接口返回之前`Backbone`对象. | |
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 ... 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; | |
// Backbone.Events | |
// --------------- | |
// A module that can be mixed in to *any object* in order to provide it with | |
// custom events. You may bind with `on` or remove with `off` callback | |
// functions to an event; `trigger`-ing an event fires all callbacks in | |
// succession. | |
// | |
// var object = {}; | |
// _.extend(object, Backbone.Events); | |
// object.on('expand', function(){ alert('expanded'); }); | |
// object.trigger('expand'); | |
// | |
// Backbone事件. | |
var Events = Backbone.Events = { | |
// Bind an event to a `callback` function. Passing `"all"` will bind | |
// the callback to all events fired. | |
// `on`函数用于绑定一个事件, 事件触发时执行回调函数`callback`. | |
// 典型调用方式是`object.on('name', callback, context)`. | |
// `name`是监听的事件名, `callback`是事件触发时的回调函数, `context`是回调函数上下文(未指定时就默认为`object`). | |
// 这里有个特殊的事件`'all'`, 绑定此事件后, 任何非`'all'`事件触发时都会执行`all`事件绑定的回调. | |
on: function(name, callback, context) { | |
// `callback`为空时直接返回. `eventsApi`调用在后面`eventsApi`的注释中再讨论. | |
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this; | |
// `object`要保存所有监听的事件, 存储在`_events`变量中. | |
this._events || (this._events = {}); | |
// 具体事件的保存是array数组, 以事件名`name`为索引. | |
var events = this._events[name] || (this._events[name] = []); | |
// 将此次绑定的事件信息压到数组尾; | |
// `callback`即事件触发时的回调函数, `ctx`是回调函数调用时的`context`, 默认为`this`(即`object`); | |
// TODO: 这里不理解为什么又要保存另外一个`context`变量. | |
events.push({callback: callback, context: context, ctx: context || this}); | |
return this; | |
}, | |
// Bind an event to only be triggered a single time. After the first time | |
// the callback is invoked, it will be removed. | |
// `once`和`on`的区别是`once`触发一次后就会解绑监听的事件, 即只执行一次. | |
once: function(name, callback, context) { | |
// 同`on` | |
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this; | |
// 保存`this`. | |
var self = this; | |
// 调用underscore的`once`函数, 确保回调只执行一次(即使出现多次调也用只会执行一次函数体). | |
// 这里其实是对传入的`callback`进行两次包裹. | |
var once = _.once(function() { | |
// 回调函数执行时, 解绑事件. 注意这里解绑的回调参数是`once`, 因为下面`on`绑定时就是`once`. | |
self.off(name, once); | |
// 调用callback函数. | |
callback.apply(this, arguments); | |
}); | |
// 设置`_callback`变量, 理解这里的用意请看后面对`off`函数的注释. | |
once._callback = callback; | |
// 调用`on`接口绑定事件. | |
return this.on(name, once, context); | |
}, | |
// 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. | |
// `off`用于解绑事件. | |
// `name`, `callback`, `context`三个参数都是可选的, 参数未指定时则匹配所有. | |
// 典型调用是`object.off('name' callback, context);`. | |
off: function(name, callback, context) { | |
// 定义一堆变量, 后面要用. | |
var retain, ev, events, names, i, l, j, k; | |
// 当前`object`不存在`_events`(即没有绑定过事件)直接返回, `eventsApi`调用在后面`eventsApi`的注释中再讨论. | |
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; | |
// 如果`name`, `callback`, `context`都为指定, 则删除所有已经绑定的事件. | |
if (!name && !callback && !context) { | |
// 这里`void 0`等价于`undefined`, 即`this._events = undefined;`. | |
this._events = void 0; | |
return this; | |
} | |
// 如果`name`未指定, 默认是针对所有已绑定的事件名. | |
names = name ? [name] : _.keys(this._events); | |
for (i = 0, l = names.length; i < l; i++) { | |
// 获取事件名称. | |
name = names[i]; | |
// 获取保存此事件的数组对象, 不为空时继续处理. | |
if (events = this._events[name]) { | |
// 直接将事件数组清空. 这里的思想是: 先清空, 然后将**不满足**条件的再加进来. | |
this._events[name] = retain = []; | |
// 任意一个不为空, 继续处理. | |
if (callback || context) { | |
for (j = 0, k = events.length; j < k; j++) { | |
// 获取事件数组中的一个事件. | |
ev = events[j]; | |
// 如果`ev.callback`与参数`callback`不同或`ev.context`与参数`context`不同, 说明此事件不需要删除. | |
// 注意这里有个`callback !== ev.callback._callback`; | |
// 回想上面的`once`函数, 里面最终`on`绑定的`callback`不是我们调用`once`时传入的那个`callback`, | |
// 而是通过underscore的`once`函数包裹后的; | |
// 即`obj.once(name, callback1)`实际`on`绑定的是包裹成的`callback2`, 且`callback2._callback = callback1;`; | |
// 所以, `once`函数中的`once._callback = callback;`语句是为了我们同样可以`off`解绑`once`绑定的事件! | |
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) || | |
(context && context !== ev.context)) { | |
// 将不满足条件的压入到事件数组. | |
retain.push(ev); | |
} | |
} | |
} | |
// 如果该事件变量数组为空, 直接在this._events中删除此事件对应的数组. | |
if (!retain.length) delete this._events[name]; | |
} | |
} | |
return this; | |
}, | |
// 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). | |
// 触发事件`name`, 执行所有绑定在事件`name`上的毁掉函数. | |
// 典型用法是`object.trigger('name', arg1, arg2, ...);`. | |
trigger: function(name) { | |
// 没有事件注册时直接返回. | |
if (!this._events) return this; | |
// 获取事件回调函数的参数. | |
var args = slice.call(arguments, 1); | |
// 见`eventsApi`的注释. | |
if (!eventsApi(this, 'trigger', name, args)) return this; | |
// 获取事件`name`的回调数组. | |
var events = this._events[name]; | |
// 获取事件`'all'`的回调数组. | |
var allEvents = this._events.all; | |
// 执行`name`事件的所有回调. | |
if (events) triggerEvents(events, args); | |
// 执行`'all'`事件的所有回调. | |
// 这里也就解释了为什么了注册了`'all'`事件后, 所有非`'all'`事件触发后都会执行`'all'`事件注册的回调. | |
if (allEvents) triggerEvents(allEvents, arguments); | |
return this; | |
}, | |
// Tell this object to stop listening to either specific events ... or | |
// to every object it's currently listening to. | |
// 解绑对对象`obj`事件`name`的监听. 与`listenTo`和`listenToOnce`函数相对的操作. | |
stopListening: function(obj, name, callback) { | |
// 获取当前已监听对象. 为空时直接返回. | |
var listeningTo = this._listeningTo; | |
if (!listeningTo) return this; | |
// 如果`name`和`callback`都为空, 则解绑对`obj`所有事件的监听. | |
var remove = !name && !callback; | |
// 如果callback是空, 且`typeof name === 'object'`, 则设置`callback = this;` | |
// 这么写原因是考虑到如下的调用方式, 看官先体会下: | |
// `object.stopListening(obj, {'name1': callback1, 'name2': callback2});`. | |
// 下面注释对上面的调用做更多说明. | |
if (!callback && typeof name === 'object') callback = this; | |
// `obj`未指定时默认是针对所有已监听的对象, 注意此函数首行的代码. | |
if (obj) (listeningTo = {})[obj._listenId] = obj; | |
for (var id in listeningTo) { | |
obj = listeningTo[id]; | |
obj.off(name, callback, this); | |
// `remove`为true(见上面的解释)或当前被监听对象已不监听任何事件, 则从监听对象中删除此`obj`. | |
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id]; | |
} | |
return this; | |
} | |
}; | |
// Regular expression used to split event strings. | |
// 正则表达式, 匹配含有空格的字符串, 如'event1 event2'. 用于下面的`eventsApi`函数. | |
var eventSplitter = /\s+/; | |
// Implement fancy features of the Events API such as multiple event | |
// names `"change blur"` and jQuery-style event maps `{change: action}` | |
// in terms of the existing API. | |
// 上面的函数代码中多次遇到此函数的调用. 这里解释下此函数的场景: | |
// 正常情况我们是通过`object.on(name, callback, context)`注册一个事件, 然而为了方便, Backbone同样支持: | |
// `object.on({name1: callback1, name2: callback2}, context);`形式的调用, 同样还支持: | |
// `object.on('name1 name2', callback, context);`形式的调用, | |
// `eventsApi`的存在就是为了将上面两种调用转成普通的形式. | |
// 函数返回false说明`eventsApi`已处理. | |
// 下面拿`on`函数中的调用方式举例说明: | |
// `eventsApi(this, 'on', name, [callback, context])`. | |
var eventsApi = function(obj, action, name, rest) { | |
// `name`为空, 直接返回true, 由调用者继续处理. | |
if (!name) return true; | |
// Handle event maps. | |
// 针对第一种形式. | |
// 如果`name`值为`{name1: callback1, name2: callback2}`, 则typeof返回就是`'object'`. | |
// `rest`为`[callback, context]`, | |
// 又因为这种绑定写法的callback已经写在`name`中, 所以实际`callback`参数用于传递`context`, 而`context`参数为空. | |
// 所以`rest`实际上等价于正常调用方式中的`[context]`. | |
if (typeof name === 'object') { | |
// 遍历上面对象的元素, 上例中`key`将分别为`'name1'`和`'name2'`. | |
for (var key in name) { | |
// 假设是`on`函数中调用此函数. 则这里转换后将是: | |
// `obj['on'].apply(obj, [name1, callback1].concat(rest));` | |
// 其中`rest`是`on`调用中的`context`, 所以最终其实下面的语句等价于: | |
// `obj.on(name1, callback1, context);` | |
obj[action].apply(obj, [key, name[key]].concat(rest)); | |
} | |
return false; | |
} | |
// Handle space separated event names. | |
// 针对第二种形式. | |
// 其实除了`{name1: callback1, name2: callback2}`形式的入参, 还支持另外一种形式的事件绑定: | |
// `obj.on('name1 name2 name3', callback, context);`, 即绑定事件'name1'和'name2'和'name3'至同一个callback. | |
// 这里name就是`'name1 name2 name3'`. 同时, 这时候`rest`是[callback, context]. | |
// 正则表达式匹配上面的`'name1 name2 name3'`. | |
if (eventSplitter.test(name)) { | |
// 将上面的`name`分割(以空格为界分割), 即最终`names = ['name1', 'name2', 'name3'];`. | |
var names = name.split(eventSplitter); | |
// 遍历`names`. | |
for (var i = 0, l = names.length; i < l; i++) { | |
// 由上, 下面的语句等价于: | |
// obj.on('name1', callback, context); | |
obj[action].apply(obj, [names[i]].concat(rest)); | |
} | |
return false; | |
} | |
return true; | |
}; | |
// 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). | |
// 触发事件的调用. | |
// 这里的`events`数组的元素是`on`调用时压入的内容, 即`on`函数中的调用: | |
// `events.push({callback: callback, context: context, ctx: context || this});` | |
var triggerEvents = function(events, args) { | |
var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2]; | |
// 这里本可以直接用`default`的处理方式, | |
// 之所以用`switch`是因为大多数的回调函数需要的参数都在三个以内(包含三个). | |
// call方式会比apply方式快. | |
switch (args.length) { | |
// `call`和`apply`函数的第一个参数都是执行`callback`函数执行时的`context`值(即函数执行体中的`this`). | |
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; | |
} | |
}; | |
// 这个变量为`listenTo`和`listenToOnce`函数定义使用 | |
var listenMethods = {listenTo: 'on', listenToOnce: 'once'}; | |
// Inversion-of-control versions of `on` and `once`. Tell *this* object to | |
// listen to an event in another object ... keeping track of what it's | |
// listening to. | |
// underscore的`each`遍历, 因为`listenTo`和`listenToOnce`的函数实现一样, `each`调用使得代码更加简洁. | |
// 下面拿`listenTo: 'on'`举例说明具体的实现方法, `listenToOnce: 'once'`原理一样, 不做特别说明. | |
_.each(listenMethods, function(implementation, method) { | |
// 这里`method`是`listenTo`, `implementation`是`on`. | |
// 即Events['listenTo'] = function(obj, name, callback) { }`. | |
// 典型调用方式是`object.listenTo(obj, name, callback);`. | |
// 其中`obj`是当前`object`想要监听的`obj`对象, `name`是监听的事件名, `callback`是监听事件触发时的回调函数. | |
Events[method] = function(obj, name, callback) { | |
// `object`需要维护已经监听的对象, 将被监听的对象存在`_listeningTo`变量中. | |
var listeningTo = this._listeningTo || (this._listeningTo = {}); | |
// 被监听对象的索引值是该对象的`_listenId`值, 这个值是唯一的. | |
var id = obj._listenId || (obj._listenId = _.uniqueId('l')); | |
// 存储被监听对象obj至`_listeningTo`中, 注: 这里`listeningTo`和`this._listeningTo`指向同一内存. | |
listeningTo[id] = obj; | |
// 如果callback是空, 且`typeof name === 'object'`, 则设置`callback = this;` | |
// 这么写原因是考虑到如下的调用方式, 先体会下: | |
// `object.listenTo(obj, {'name1': callback1, 'name2': callback2});`. | |
// 下面注释对上面的调用做更多说明. | |
if (!callback && typeof name === 'object') callback = this; | |
// `obj['on'](name, callback, this);`. | |
// 到这里发现原来最终是调用`obj`的`on`接口, 只是在调用时设置`context`(第三个参数)为`this`(即监听`object`). | |
// 这里回味下`obj['on']({'name1': callback1, 'name2': callback2});`, | |
// 再回到`object.listenTo(obj, {'name1': callback1, 'name2': callback2});`. | |
// 如果上面不设置`callback = this`, 再`obj['on']({'name1': callback1, 'name2': callback2});`时, 下面的调用就相当于: | |
// `obj['on']({'name1': callback1, 'name2': callback2}, null, this);` | |
// 这样的话, 其实`on`调用会把第二个参数(即null)当做`context`传递, | |
// 因为传递值为`null`, 所以`on`调用最终会将`obj`自身设置`context`, | |
// 即最终相当于`obj`自身监听事件, 而不是`object`在监听, | |
// 最终导致的结果就是事件触发后`callback`函数调用时内部的`this`指向`obj`而不是`object`. | |
// 这就是上面有个`callback = this;`语句的原因! | |
obj[implementation](name, callback, this); | |
return this; | |
}; | |
}); | |
// Aliases for backwards compatibility. | |
// alias操作, 即object.on和object.bind等价, object.off和object.unbind等价. | |
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. | |
// 将Events的特性全部extend到Backbone, 即Backbone也可以做Backbone.on/Backbone.trigger这样的操作. | |
_.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. | |
// Backbone.Model的定义. | |
// 通常通过Backbone.Model.extend()函数创建一个子Model. | |
var Model = Backbone.Model = function(attributes, options) { | |
// new一个对象时, `attrs`保存传入的model数据. | |
var attrs = attributes || {}; | |
options || (options = {}); | |
// model的唯一标识`cid`. | |
this.cid = _.uniqueId('c'); | |
// model模型元数据都存储在`attributes`变量中. | |
this.attributes = {}; | |
// 如果指定`collection`则保存, model在构造url时可能会用到此参数. | |
if (options.collection) this.collection = options.collection; | |
// 如果明确指出需要`parse`, 将传入的数据解析成model的格式. | |
if (options.parse) attrs = this.parse(attrs, options) || {}; | |
// 设置model中变量的默认值(`defaults`中保存了默认值); | |
attrs = _.defaults({}, attrs, _.result(this, 'defaults')); | |
// 调用`set`设置数据到`this.attributes`中. | |
this.set(attrs, options); | |
// 用于保存上一次`set`之后改变的数据字段. | |
this.changed = {}; | |
// 执行初始化函数`initialize`. | |
this.initialize.apply(this, arguments); | |
}; | |
// Attach all inheritable methods to the Model prototype. | |
// `extend` Model的`prototype`属性. | |
_.extend(Model.prototype, Events, { | |
// A hash of attributes whose current and previous value differ. | |
// 用于保存上一次`set`之后改变的数据字段, 在new一个对象时此变量值会被改成{}. | |
changed: null, | |
// The value returned during the last failed validation. | |
// 如果数据字段的格式不合法, 此变量不为空. 可通过此变量判断数据有效性. | |
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', | |
// Initialize is an empty function by default. Override it with your own | |
// initialization logic. | |
initialize: function(){}, | |
// Return a copy of the model's `attributes` object. | |
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. | |
sync: function() { | |
return Backbone.sync.apply(this, arguments); | |
}, | |
// Get the value of an attribute. | |
get: function(attr) { | |
return this.attributes[attr]; | |
}, | |
// Get the HTML-escaped value of an attribute. | |
escape: function(attr) { | |
return _.escape(this.get(attr)); | |
}, | |
// Returns `true` if the attribute contains a value that is not null | |
// or undefined. | |
has: function(attr) { | |
return this.get(attr) != null; | |
}, | |
// Set a hash of model attributes on the object, firing `"change"`. This is | |
// the core primitive operation of a model, updating the data and notifying | |
// anyone who needs to know about the change in state. The heart of the beast. | |
set: function(key, val, options) { | |
var attr, attrs, unset, changes, silent, changing, prev, current; | |
if (key == null) return this; | |
// Handle both `"key", value` and `{key: value}` -style arguments. | |
if (typeof key === 'object') { | |
attrs = key; | |
options = val; | |
} else { | |
(attrs = {})[key] = val; | |
} | |
options || (options = {}); | |
// Run validation. | |
if (!this._validate(attrs, options)) return false; | |
// Extract attributes and options. | |
unset = options.unset; | |
silent = options.silent; | |
changes = []; | |
changing = this._changing; | |
this._changing = true; | |
if (!changing) { | |
this._previousAttributes = _.clone(this.attributes); | |
this.changed = {}; | |
} | |
current = this.attributes, prev = this._previousAttributes; | |
// Check for changes of `id`. | |
if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; | |
// For each `set` attribute, update or delete the current value. | |
for (attr in attrs) { | |
val = attrs[attr]; | |
if (!_.isEqual(current[attr], val)) changes.push(attr); | |
if (!_.isEqual(prev[attr], val)) { | |
this.changed[attr] = val; | |
} else { | |
delete this.changed[attr]; | |
} | |
unset ? delete current[attr] : current[attr] = val; | |
} | |
// Trigger all relevant attribute changes. | |
if (!silent) { | |
if (changes.length) this._pending = options; | |
for (var i = 0, l = changes.length; i < l; 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. | |
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; | |
}, | |
// Remove an attribute from the model, firing `"change"`. `unset` is a noop | |
// if the attribute doesn't exist. | |
unset: function(attr, options) { | |
return this.set(attr, void 0, _.extend({}, options, {unset: true})); | |
}, | |
// Clear all attributes on the model, firing `"change"`. | |
clear: function(options) { | |
var attrs = {}; | |
for (var key in this.attributes) attrs[key] = void 0; | |
return this.set(attrs, _.extend({}, options, {unset: true})); | |
}, | |
// Determine if the model has changed since the last `"change"` event. | |
// If you specify an attribute name, determine if that attribute has changed. | |
hasChanged: function(attr) { | |
if (attr == null) return !_.isEmpty(this.changed); | |
return _.has(this.changed, attr); | |
}, | |
// Return an object containing all the attributes that have changed, or | |
// false if there are no changed attributes. Useful for determining what | |
// parts of a view need to be updated and/or what attributes need to be | |
// persisted to the server. Unset attributes will be set to undefined. | |
// You can also pass an attributes object to diff against the model, | |
// determining if there *would be* a change. | |
changedAttributes: function(diff) { | |
if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; | |
var val, changed = false; | |
var old = this._changing ? this._previousAttributes : this.attributes; | |
for (var attr in diff) { | |
if (_.isEqual(old[attr], (val = diff[attr]))) continue; | |
(changed || (changed = {}))[attr] = val; | |
} | |
return changed; | |
}, | |
// Get the previous value of an attribute, recorded at the time the last | |
// `"change"` event was fired. | |
previous: function(attr) { | |
if (attr == null || !this._previousAttributes) return null; | |
return this._previousAttributes[attr]; | |
}, | |
// Get all of the attributes of the model at the time of the previous | |
// `"change"` event. | |
previousAttributes: function() { | |
return _.clone(this._previousAttributes); | |
}, | |
// Fetch the model from the server. If the server's representation of the | |
// model differs from its current attributes, they will be overridden, | |
// triggering a `"change"` event. | |
fetch: function(options) { | |
options = options ? _.clone(options) : {}; | |
if (options.parse === void 0) options.parse = true; | |
var model = this; | |
var success = options.success; | |
options.success = function(resp) { | |
if (!model.set(model.parse(resp, options), options)) return false; | |
if (success) success(model, resp, options); | |
model.trigger('sync', model, resp, options); | |
}; | |
wrapError(this, options); | |
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. | |
save: function(key, val, options) { | |
var attrs, method, xhr, attributes = this.attributes; | |
// Handle both `"key", value` and `{key: value}` -style arguments. | |
if (key == null || typeof key === 'object') { | |
attrs = key; | |
options = val; | |
} else { | |
(attrs = {})[key] = val; | |
} | |
options = _.extend({validate: true}, options); | |
// 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. | |
if (attrs && !options.wait) { | |
if (!this.set(attrs, options)) return false; | |
} else { | |
if (!this._validate(attrs, options)) return false; | |
} | |
// Set temporary attributes if `{wait: true}`. | |
if (attrs && options.wait) { | |
this.attributes = _.extend({}, attributes, attrs); | |
} | |
// After a successful server-side save, the client is (optionally) | |
// updated with the server-side state. | |
if (options.parse === void 0) options.parse = true; | |
var model = this; | |
var success = options.success; | |
options.success = function(resp) { | |
// Ensure attributes are restored during synchronous saves. | |
model.attributes = attributes; | |
var serverAttrs = model.parse(resp, options); | |
if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); | |
if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { | |
return false; | |
} | |
if (success) success(model, resp, options); | |
model.trigger('sync', model, resp, options); | |
}; | |
wrapError(this, options); | |
method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); | |
if (method === 'patch') options.attrs = attrs; | |
xhr = this.sync(method, this, options); | |
// Restore attributes. | |
if (attrs && options.wait) 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. | |
destroy: function(options) { | |
options = options ? _.clone(options) : {}; | |
var model = this; | |
var success = options.success; | |
var destroy = function() { | |
model.trigger('destroy', model, model.collection, options); | |
}; | |
options.success = function(resp) { | |
if (options.wait || model.isNew()) destroy(); | |
if (success) success(model, resp, options); | |
if (!model.isNew()) model.trigger('sync', model, resp, options); | |
}; | |
if (this.isNew()) { | |
options.success(); | |
return false; | |
} | |
wrapError(this, options); | |
var xhr = this.sync('delete', this, options); | |
if (!options.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. | |
url: function() { | |
var base = | |
_.result(this, 'urlRoot') || | |
_.result(this.collection, 'url') || | |
urlError(); | |
if (this.isNew()) return base; | |
return base.replace(/([^\/])$/, '$1/') + encodeURIComponent(this.id); | |
}, | |
// **parse** converts a response into the hash of attributes to be `set` on | |
// the model. The default implementation is just to pass the response along. | |
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. | |
isNew: function() { | |
return !this.has(this.idAttribute); | |
}, | |
// Check if the model is currently in a valid state. | |
isValid: function(options) { | |
return this._validate({}, _.extend(options || {}, { validate: true })); | |
}, | |
// Run validation against the next complete set of model attributes, | |
// returning `true` if all is well. Otherwise, fire an `"invalid"` event. | |
_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. | |
// 为`Model`引入一些underscore的功能函数 | |
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit']; | |
// Mix in each Underscore method as a proxy to `Model#attributes`. | |
// 遍历`modelMethods` | |
_.each(modelMethods, function(method) { | |
Model.prototype[method] = function() { | |
var args = slice.call(arguments); | |
args.unshift(this.attributes); | |
return _[method].apply(_, args); | |
}; | |
}); | |
// Backbone.Collection | |
// ------------------- | |
// If models tend to represent a single row of data, a Backbone Collection is | |
// more analagous 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. | |
var Collection = Backbone.Collection = function(models, options) { | |
options || (options = {}); | |
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)); | |
}; | |
// Default options for `Collection#set`. | |
var setOptions = {add: true, remove: true, merge: true}; | |
var addOptions = {add: true, remove: false}; | |
// Define the Collection's inheritable methods. | |
_.extend(Collection.prototype, Events, { | |
// The default model for a collection is just a **Backbone.Model**. | |
// This should be overridden in most cases. | |
model: Model, | |
// Initialize is an empty function by default. Override it with your own | |
// initialization logic. | |
initialize: function(){}, | |
// The JSON representation of a Collection is an array of the | |
// models' attributes. | |
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. | |
add: function(models, options) { | |
return this.set(models, _.extend({merge: false}, options, addOptions)); | |
}, | |
// Remove a model, or a list of models from the set. | |
remove: function(models, options) { | |
var singular = !_.isArray(models); | |
models = singular ? [models] : _.clone(models); | |
options || (options = {}); | |
var i, l, index, model; | |
for (i = 0, l = models.length; i < l; i++) { | |
model = models[i] = this.get(models[i]); | |
if (!model) continue; | |
delete this._byId[model.id]; | |
delete this._byId[model.cid]; | |
index = this.indexOf(model); | |
this.models.splice(index, 1); | |
this.length--; | |
if (!options.silent) { | |
options.index = index; | |
model.trigger('remove', model, this, options); | |
} | |
this._removeReference(model, options); | |
} | |
return singular ? models[0] : models; | |
}, | |
// 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. | |
set: function(models, options) { | |
options = _.defaults({}, options, setOptions); | |
if (options.parse) models = this.parse(models, options); | |
var singular = !_.isArray(models); | |
models = singular ? (models ? [models] : []) : _.clone(models); | |
var i, l, id, model, attrs, existing, sort; | |
var at = options.at; | |
var targetModel = this.model; | |
var sortable = this.comparator && (at == null) && options.sort !== false; | |
var sortAttr = _.isString(this.comparator) ? this.comparator : null; | |
var toAdd = [], toRemove = [], modelMap = {}; | |
var add = options.add, merge = options.merge, remove = options.remove; | |
var order = !sortable && add && remove ? [] : false; | |
// Turn bare objects into model references, and prevent invalid models | |
// from being added. | |
for (i = 0, l = models.length; i < l; i++) { | |
attrs = models[i] || {}; | |
if (attrs instanceof Model) { | |
id = model = attrs; | |
} else { | |
id = attrs[targetModel.prototype.idAttribute || 'id']; | |
} | |
// If a duplicate is found, prevent it from being added and | |
// optionally merge it into the existing model. | |
if (existing = this.get(id)) { | |
if (remove) modelMap[existing.cid] = true; | |
if (merge) { | |
attrs = attrs === model ? model.attributes : attrs; | |
if (options.parse) attrs = existing.parse(attrs, options); | |
existing.set(attrs, options); | |
if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; | |
} | |
models[i] = existing; | |
// If this is a new, valid model, push it to the `toAdd` list. | |
} else if (add) { | |
model = models[i] = this._prepareModel(attrs, options); | |
if (!model) continue; | |
toAdd.push(model); | |
this._addReference(model, options); | |
} | |
// Do not add multiple models with the same `id`. | |
model = existing || model; | |
if (order && (model.isNew() || !modelMap[model.id])) order.push(model); | |
modelMap[model.id] = true; | |
} | |
// Remove nonexistent models if appropriate. | |
if (remove) { | |
for (i = 0, l = this.length; i < l; ++i) { | |
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); | |
} | |
if (toRemove.length) this.remove(toRemove, options); | |
} | |
// See if sorting is needed, update `length` and splice in new models. | |
if (toAdd.length || (order && order.length)) { | |
if (sortable) sort = true; | |
this.length += toAdd.length; | |
if (at != null) { | |
for (i = 0, l = toAdd.length; i < l; i++) { | |
this.models.splice(at + i, 0, toAdd[i]); | |
} | |
} else { | |
if (order) this.models.length = 0; | |
var orderedModels = order || toAdd; | |
for (i = 0, l = orderedModels.length; i < l; i++) { | |
this.models.push(orderedModels[i]); | |
} | |
} | |
} | |
// Silently sort the collection if appropriate. | |
if (sort) this.sort({silent: true}); | |
// Unless silenced, it's time to fire all appropriate add/sort events. | |
if (!options.silent) { | |
for (i = 0, l = toAdd.length; i < l; i++) { | |
(model = toAdd[i]).trigger('add', model, this, options); | |
} | |
if (sort || (order && order.length)) this.trigger('sort', this, options); | |
} | |
// Return the added (or merged) model (or 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: function(models, options) { | |
options || (options = {}); | |
for (var i = 0, l = this.models.length; i < l; 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; | |
}, | |
// Add a model to the end of the collection. | |
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); | |
this.remove(model, options); | |
return model; | |
}, | |
// Add a model to the beginning of the collection. | |
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); | |
this.remove(model, options); | |
return model; | |
}, | |
// Slice out a sub-array of models from the collection. | |
slice: function() { | |
return slice.apply(this.models, arguments); | |
}, | |
// Get a model from the set by id. | |
get: function(obj) { | |
if (obj == null) return void 0; | |
return this._byId[obj] || this._byId[obj.id] || this._byId[obj.cid]; | |
}, | |
// Get the model at the given index. | |
at: function(index) { | |
return this.models[index]; | |
}, | |
// Return models with matching attributes. Useful for simple cases of | |
// `filter`. | |
where: function(attrs, first) { | |
if (_.isEmpty(attrs)) return first ? void 0 : []; | |
return this[first ? 'find' : 'filter'](function(model) { | |
for (var key in attrs) { | |
if (attrs[key] !== model.get(key)) return false; | |
} | |
return true; | |
}); | |
}, | |
// 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. | |
sort: function(options) { | |
if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); | |
options || (options = {}); | |
// Run sort based on type of `comparator`. | |
if (_.isString(this.comparator) || this.comparator.length === 1) { | |
this.models = this.sortBy(this.comparator, this); | |
} else { | |
this.models.sort(_.bind(this.comparator, this)); | |
} | |
if (!options.silent) this.trigger('sort', this, options); | |
return this; | |
}, | |
// Pluck an attribute from each model in the collection. | |
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`. | |
fetch: function(options) { | |
options = options ? _.clone(options) : {}; | |
if (options.parse === void 0) options.parse = true; | |
var success = options.success; | |
var collection = this; | |
options.success = function(resp) { | |
var method = options.reset ? 'reset' : 'set'; | |
collection[method](resp, options); | |
if (success) success(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. | |
create: function(model, options) { | |
options = options ? _.clone(options) : {}; | |
if (!(model = this._prepareModel(model, options))) return false; | |
if (!options.wait) this.add(model, options); | |
var collection = this; | |
var success = options.success; | |
options.success = function(model, resp) { | |
if (options.wait) collection.add(model, options); | |
if (success) success(model, resp, options); | |
}; | |
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: 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); | |
}, | |
// Private method to reset all internal state. Called when the collection | |
// is first initialized or reset. | |
_reset: function() { | |
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) { | |
if (attrs instanceof Model) return attrs; | |
options = options ? _.clone(options) : {}; | |
options.collection = this; | |
var model = new this.model(attrs, options); | |
if (!model.validationError) return model; | |
this.trigger('invalid', this, model.validationError, options); | |
return false; | |
}, | |
// Internal method to create a model's ties to a collection. | |
_addReference: function(model, options) { | |
this._byId[model.cid] = model; | |
if (model.id != null) this._byId[model.id] = model; | |
if (!model.collection) model.collection = this; | |
model.on('all', this._onModelEvent, this); | |
}, | |
// Internal method to sever a model's ties to a collection. | |
_removeReference: function(model, options) { | |
if (this === model.collection) delete model.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. | |
_onModelEvent: function(event, model, collection, options) { | |
if ((event === 'add' || event === 'remove') && collection !== this) return; | |
if (event === 'destroy') this.remove(model, options); | |
if (model && event === 'change:' + model.idAttribute) { | |
delete this._byId[model.previous(model.idAttribute)]; | |
if (model.id != null) this._byId[model.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 methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl', | |
'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select', | |
'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke', | |
'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest', | |
'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle', | |
'lastIndexOf', 'isEmpty', 'chain', 'sample']; | |
// Mix in each Underscore method as a proxy to `Collection#models`. | |
_.each(methods, function(method) { | |
Collection.prototype[method] = function() { | |
var args = slice.call(arguments); | |
args.unshift(this.models); | |
return _[method].apply(_, args); | |
}; | |
}); | |
// Underscore methods that take a property name as an argument. | |
var attributeMethods = ['groupBy', 'countBy', 'sortBy', 'indexBy']; | |
// Use attributes instead of properties. | |
_.each(attributeMethods, function(method) { | |
Collection.prototype[method] = function(value, context) { | |
var iterator = _.isFunction(value) ? value : function(model) { | |
return model.get(value); | |
}; | |
return _[method](this.models, iterator, context); | |
}; | |
}); | |
// 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... | |
// `Bacbone.View`定义. | |
var View = Backbone.View = function(options) { | |
// 每个view都存在一个`cid`标识(值唯一). | |
this.cid = _.uniqueId('view'); | |
options || (options = {}); | |
// 利用underscore的`pick`函数从`options`中extend `viewOptions`指定的参数. | |
_.extend(this, _.pick(options, viewOptions)); | |
// 确保当前view是有效的, 具体实现看`_ensureElement`的注释. | |
this._ensureElement(); | |
// 调用初始化函数`initialize`. | |
this.initialize.apply(this, arguments); | |
// 绑定DOM事件(这里的事件绑定和Backbone自身的Events不同, 而是利用jQuery实现). | |
this.delegateEvents(); | |
}; | |
// Cached regex to split keys for `delegate`. | |
// 正则表达式, 用于事件绑定函数`delegateEvents`. | |
// 此正则表达式匹配**以空格分割的字符串**, 如"click #id.post". | |
var delegateEventSplitter = /^(\S+)\s*(.*)$/; | |
// List of view options to be merged as properties. | |
// 此变量使用见上面的构造函数, view在new时可以提供参数, | |
// 而Backbone只会提取`viewOptions`中指定的参数, 其他的会忽略. | |
// model: view对应的model**对象**; | |
// collection: view对应的collection**对象**; | |
// el: 直接指定view对应的DOM, 可能的形式是: $('#id') 或 '#id' (Backbone会兼容这两种值); | |
// id: view所在DOM的`id`属性; | |
// attributes: DOM对象的属性配置, 如`input`标签的`type`, `img`标签的`src`, | |
// 甚至`class`等也可以在这里设置而不用`className`; | |
// className: view所在DOM的`class`属性; | |
// tagName: view对应的DOM标签tag, 默认是`div`; | |
// events: 需要绑定的事件信息. | |
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'`. | |
tagName: 'div', | |
// jQuery delegate for element lookup, scoped to DOM elements within the | |
// current view. This should be preferred to global lookups where possible. | |
// 为view对象提供$函数(利用jQuery的find接口). | |
$: 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函数, 实际使用中这个函数用于渲染view实例对应的界面. | |
render: function() { | |
return this; | |
}, | |
// Remove this view by taking the element out of the DOM, and removing any | |
// applicable Backbone.Events listeners. | |
// 销毁函数. | |
remove: function() { | |
// 删除DOM元素. | |
this.$el.remove(); | |
// 取消绑定的事件. | |
this.stopListening(); | |
return this; | |
}, | |
// Change the view's element (`this.el` property), including event | |
// re-delegation. | |
// 改变view对应的DOM. 默认会重新绑定事件. | |
setElement: function(element, delegate) { | |
// 如果之前的DOM已经存在, 则解绑之前的事件. | |
if (this.$el) this.undelegateEvents(); | |
// 根据`element`的类型改变`$el`对象, 注意最终`$el`是一个jQuery对象. | |
this.$el = element instanceof Backbone.$ ? element : Backbone.$(element); | |
// `el`变量是一个DOM元素(类型与getElementById的返回值, 具体可以参见jQuery对象的格式). | |
this.el = this.$el[0]; | |
// 如果没有特别说明, 重新绑定事件到新DOM元素. | |
if (delegate !== false) this.delegateEvents(); | |
return this; | |
}, | |
// 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 only works for delegate-able events: not `focus`, `blur`, and | |
// not `change`, `submit`, and `reset` in Internet Explorer. | |
// 绑定事件的实现, `events`的参数格式参见上面的英文注释. | |
delegateEvents: function(events) { | |
// 这里有个underscore的`_.result`的调用, Backbone中存在很多这样的调用, 这里举例解释下`_.result`的好处: | |
// 默认情况, 如果`'event'`对象就是普通的Object对象, 则直接返回; | |
// 特殊情况是如果`'event'`是一个函数, `_.result`函数就会将函数的执行结果返回! | |
if (!(events || (events = _.result(this, 'events')))) return this; | |
// 事件绑定前线解绑之前已绑定的事件. | |
this.undelegateEvents(); | |
// 遍历`events`对象. | |
for (var key in events) { | |
// 获取一个事件配置. | |
var method = events[key]; | |
// 从函数开头的注释中可以看出, 事件的回调本应该是一个函数, 但Backbone同样允许你给一个字符串. | |
// 因为这里会检测, 如果是字符串, 就以当前对象中以指定字符串为索引的对象为回调函数. | |
if (!_.isFunction(method)) method = this[events[key]]; | |
// 上面的操作后发现回调函数不存在, 直接跳过. | |
if (!method) continue; | |
// 正则匹配事件配置信息. | |
// 举例: 'mousedown .title': 'edit' | |
// 这里的key就是上面的'mousedown .title' | |
var match = key.match(delegateEventSplitter); | |
// `match[1]`就是上面的'mousedown'; | |
// `match[2]`就是上面的'.title'; | |
var eventName = match[1], selector = match[2]; | |
// 利用underscore的`bind`函数指定回调函数的上下文`context`为当前view(这里的`this`); | |
method = _.bind(method, this); | |
// 每个事件都加一个`'.delegateEvents' + this.cid`后缀, 即`namespaces`概念, 这样在事件解绑的时候很方便. | |
// 这是jQuery的特性, 具体参见jQeury的`on`函数. | |
eventName += '.delegateEvents' + this.cid; | |
// `selector`为空时表示事件是针对当前view. | |
// 否则就是针对当前view的子元素(子DOM). | |
if (selector === '') { | |
this.$el.on(eventName, method); | |
} else { | |
this.$el.on(eventName, selector, method); | |
} | |
} | |
return this; | |
}, | |
// Clears all callbacks previously bound to the view with `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() { | |
this.$el.off('.delegateEvents' + this.cid); | |
return this; | |
}, | |
// 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() { | |
// 如果已经指定`el`属性, 则当前view的DOM根据`el`属性获取; | |
// 否则根据其他诸如`id`, `className`, `tagName`获取; | |
if (!this.el) { | |
// 获取DOM的属性配置. | |
var attrs = _.extend({}, _.result(this, 'attributes')); | |
// 如果设置了`id`, 则将`id`值添加到DOM属性配置对象中. | |
if (this.id) attrs.id = _.result(this, 'id'); | |
// 如果设置了`className`, 则将`className`值添加到DOM属性配置对象中. | |
if (this.className) attrs['class'] = _.result(this, 'className'); | |
// 新建DOM对象, 并设置属性. | |
var $el = Backbone.$('<' + _.result(this, 'tagName') + '>').attr(attrs); | |
// 设置新建的jQuery对象至当前view. | |
this.setElement($el, false); | |
} else { | |
this.setElement(_.result(this, 'el'), false); | |
} | |
} | |
}); | |
// 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.sync = function(method, model, options) { | |
var type = methodMap[method]; | |
// Default options, unless specified. | |
_.defaults(options || (options = {}), { | |
emulateHTTP: Backbone.emulateHTTP, | |
emulateJSON: Backbone.emulateJSON | |
}); | |
// Default JSON-request options. | |
var params = {type: type, dataType: 'json'}; | |
// Ensure that we have a URL. | |
if (!options.url) { | |
params.url = _.result(model, 'url') || urlError(); | |
} | |
// Ensure that we have the appropriate request data. | |
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { | |
params.contentType = 'application/json'; | |
params.data = JSON.stringify(options.attrs || model.toJSON(options)); | |
} | |
// For older servers, emulate JSON by encoding the request into an HTML-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. | |
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. | |
if (params.type !== 'GET' && !options.emulateJSON) { | |
params.processData = false; | |
} | |
// If we're sending a `PATCH` request, and we're in an old Internet Explorer | |
// that still has ActiveX enabled by default, override jQuery to use that | |
// for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. | |
if (params.type === 'PATCH' && noXhrPatch) { | |
params.xhr = function() { | |
return new ActiveXObject("Microsoft.XMLHTTP"); | |
}; | |
} | |
// Make the request, allowing the user to override any Ajax options. | |
var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); | |
model.trigger('request', model, xhr, options); | |
return xhr; | |
}; | |
var noXhrPatch = | |
typeof window !== 'undefined' && !!window.ActiveXObject && | |
!(window.XMLHttpRequest && (new XMLHttpRequest).dispatchEvent); | |
// Map from CRUD to HTTP for our default `Backbone.sync` implementation. | |
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. | |
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. | |
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) { | |
// ... | |
// }); | |
// | |
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); | |
router.execute(callback, args); | |
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) { | |
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. | |
_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]); | |
} | |
}, | |
// 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. | |
_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. | |
var History = Backbone.History = function() { | |
// 保存所有route信息 | |
this.handlers = []; | |
// 绑定`checkUrl`函数的`context` | |
_.bindAll(this, 'checkUrl'); | |
// Ensure that `History` can be used outside of the browser. | |
// 获取浏览器环境的`location`和`history`对象. | |
if (typeof window !== 'undefined') { | |
this.location = window.location; | |
this.history = window.history; | |
} | |
}; | |
// Cached regex for stripping a leading hash/slash and trailing space. | |
// 正则表达式, 匹配以`#`或`/`开头或以空格结尾的字符串. | |
var routeStripper = /^[#\/]|\s+$/g; | |
// Cached regex for stripping leading and trailing slashes. | |
// 正则表达式, 匹配以一个及以上`/`开头或以一个及以上`/`结尾的字符串. | |
var rootStripper = /^\/+|\/+$/g; | |
// Cached regex for detecting MSIE. | |
// 正则表达式, 匹配含有`msie `(且之后不为空)子串的字符串. | |
var isExplorer = /msie [\w.]+/; | |
// Cached regex for removing a trailing slash. | |
// 正则表达式, 匹配以`/`结尾的字符串. | |
var trailingSlash = /\/$/; | |
// 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() { | |
return this.location.pathname.replace(/[^\/]$/, '$&/') === this.root; | |
}, | |
// 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 cross-browser normalized URL fragment, either from the URL, | |
// the hash, or the override. | |
getFragment: function(fragment, forcePushState) { | |
if (fragment == null) { | |
if (this._hasPushState || !this._wantsHashChange || forcePushState) { | |
fragment = decodeURI(this.location.pathname + this.location.search); | |
var root = this.root.replace(trailingSlash, ''); | |
if (!fragment.indexOf(root)) fragment = fragment.slice(root.length); | |
} 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. | |
start: function(options) { | |
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._wantsPushState = !!this.options.pushState; | |
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); | |
var fragment = this.getFragment(); | |
var docMode = document.documentMode; | |
var oldIE = (isExplorer.exec(navigator.userAgent.toLowerCase()) && (!docMode || docMode <= 7)); | |
// Normalize root to always include a leading and trailing slash. | |
this.root = ('/' + this.root + '/').replace(rootStripper, '/'); | |
if (oldIE && this._wantsHashChange) { | |
var frame = Backbone.$('<iframe src="javascript:0" tabindex="-1">'); | |
this.iframe = frame.hide().appendTo('body')[0].contentWindow; | |
this.navigate(fragment); | |
} | |
// Depending on whether we're using pushState or hashes, and whether | |
// 'onhashchange' is supported, determine how we check the URL state. | |
if (this._hasPushState) { | |
Backbone.$(window).on('popstate', this.checkUrl); | |
} else if (this._wantsHashChange && ('onhashchange' in window) && !oldIE) { | |
Backbone.$(window).on('hashchange', this.checkUrl); | |
} else if (this._wantsHashChange) { | |
this._checkUrlInterval = setInterval(this.checkUrl, this.interval); | |
} | |
// Determine if we need to change the base url, for a pushState link | |
// opened by a non-pushState browser. | |
this.fragment = fragment; | |
var loc = this.location; | |
// 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()) { | |
this.fragment = this.getFragment(null, true); | |
this.location.replace(this.root + '#' + this.fragment); | |
// 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() && loc.hash) { | |
this.fragment = this.getHash().replace(routeStripper, ''); | |
this.history.replaceState({}, document.title, this.root + this.fragment); | |
} | |
} | |
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() { | |
Backbone.$(window).off('popstate', this.checkUrl).off('hashchange', this.checkUrl); | |
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 (current === this.fragment && this.iframe) { | |
current = this.getFragment(this.getHash(this.iframe)); | |
} | |
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) { | |
fragment = this.fragment = this.getFragment(fragment); | |
return _.any(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}; | |
var url = this.root + (fragment = this.getFragment(fragment || '')); | |
// Strip the hash for matching. | |
fragment = fragment.replace(pathStripper, ''); | |
if (this.fragment === fragment) return; | |
this.fragment = fragment; | |
// Don't include a trailing slash on the root. | |
if (fragment === '' && url !== '/') url = url.slice(0, -1); | |
// If pushState is available, we use it to set the fragment as a real URL. | |
if (this._hasPushState) { | |
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.getFragment(this.getHash(this.iframe)))) { | |
// 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) this.iframe.document.open().close(); | |
this._updateHash(this.iframe.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. | |
// Backbone.extend函数, 类似于工厂函数, 维持原型链. | |
var extend = function(protoProps, staticProps) { | |
var parent = this; | |
var child; | |
// The constructor function for the new subclass is either defined by you | |
// (the "constructor" property in your `extend` definition), or defaulted | |
// by us to simply call the parent's constructor. | |
// 子类的构造函数: 如果`protoProps`参数中指定则使用指定的, 否则使用默认的. | |
if (protoProps && _.has(protoProps, 'constructor')) { | |
child = protoProps.constructor; | |
} else { | |
// 默认的构造函数中会调用父类的构造函数, 以实现基本初始化. | |
child = function(){ return parent.apply(this, arguments); }; | |
} | |
// Add static properties to the constructor function, if supplied. | |
// 将父类和参数`staticProps`的属性添加到子类. | |
// 典型的如父类的`extend`函数. | |
_.extend(child, parent, staticProps); | |
// Set the prototype chain to inherit from `parent`, without calling | |
// `parent`'s constructor function. | |
// 此处是处理原型链. extend出子类的`prototype`需要直接或简介指向父类的`prototype`属性. | |
// 如果是直接`child.prototype = parent.prototype;`那么所有子类都共享一个`prototype`, | |
// 那么对任一子类`prototype`属性的更改都将影响到父类及其他子类. | |
// 所以这里使用了一个代理. | |
var Surrogate = function(){ this.constructor = child; }; | |
Surrogate.prototype = parent.prototype; | |
child.prototype = new Surrogate; | |
// Add prototype properties (instance properties) to the subclass, | |
// if supplied. | |
// 将入参`protoProps`的属性添加到子类的`prototype`对象中. | |
if (protoProps) _.extend(child.prototype, protoProps); | |
// Set a convenience property in case the parent's prototype is needed | |
// later. | |
// 因为多了一个代理(`Surrogate`), 最终产生的子类其实是`Surrogate`直接子类. | |
// 子类保存一个`__super__`, 直接指向真实父类的`prototype`. | |
child.__super__ = parent.prototype; | |
return child; | |
}; | |
// Set up inheritance for the model, collection, router, view and history. | |
// 为Model/Collection/Router/View/History增加`extend`特性以方便生成子类. | |
Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend; | |
// Throw an error when a URL is needed, and none is supplied. | |
// Model/Collection在发送读写操作时如果未指定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. | |
// 为Model/Collection包装一个error的callback, 当数据读写失败时触发`error`事件. | |
var wrapError = function(model, options) { | |
var error = options.error; | |
options.error = function(resp) { | |
if (error) error(model, resp, options); | |
model.trigger('error', model, resp, options); | |
}; | |
}; | |
return Backbone; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment