Backbone


Bockbone 总览

Backbone 与 MVC

模式:解决某一类问题的通用方案 - 套路

MVC:一种架构模式,解耦代码,分离关注点

M(Model) - 数据模型 V(View) - 表现视图 C(Controller) - 控制器

Backbone 与 SPA

传统web应用与 SPA 的区别:

状态概念代替了页面概念

http://www.example.com/page1
http://www.example.com/page2 http://www.example.com/#/state1
http://www.example.com/#/state2

Backbone 核心

(1)Backbone 命名空间下提供五个类:

  • Backbone.Model
  • Backbone.Collection
  • Backbone.View
  • Backbone.Router
  • Backbone.History

每个类都可以实例化:

var model1 = new Backbone.Model({name: 'Alice'});
var model2 = new Backbone.Model({name: 'Brooks'}); var collecion = new Collection();
collection.add(model1);
collection.add(model2); /*
注意: Backbone.History 负责整个页面的历史记录和导航行为管理,因此在整个生命周期内应该保持唯一,Backbone.history 引用这个唯一的 Backbone.History 实例:
*/
Backbone.history.start();

(2)使用 extend( protoProp, staticProp ) 方法扩展子类:

// extend() 方法第一个参数是需要扩展的实例方法,第二个参数是需要扩展的静态方法
var Man = Backbone.Model.extend({
name: 'Jack',
intro: function() {
alert('My name is ' + this.name + '.');
}
}, {
race: 'Human'
}); var man = new Man();
console.log(man.name); // Jack
man.intro(); // My name is Jack.
console.log(Man.race); // Human

通过 extend() 方法扩展出来的子类,原型继承于父类,但是在实例化时不会调用父类的构造方法:

Man.prototype instanceof Backbone.Model;    // true
Man.__super__ === Backbone.Model.prototype; // true

(3)数据持久化支持:

Backbone 数据的持久化支持由全局方法 Backbone.sync() 提供,该方法默认使用 $.ajax() 方法发送 RESTful 请求的方式同步数据到后端服务器:

REST风格的请求:
create → POST /collection
read → GET /collection[/id]
update → PUT /collection/id
patch → PATCH /collection/id
delete → DELETE /collection/id

用户也可以通过重写 Backbone.sync() 方法采用不同的持久化方案,如 LocalStorage:

/*
Backbone.sync(method, model, [options]):
method – CRUD 方法 ("create", "read", "update", or "delete")
model – 要被保存的模型(或要被读取的集合)
options – 成功和失败的回调函数,以及所有 jQuery 请求支持的选项
*/
Backbone.sync = function(method, model, options){
switch(method){
case 'create': localStorage.setItem(model.cid,JSON.stringify(model));break;
case 'update': localStorage.setItem(model.cid,JSON.stringify(model));break;
case 'read': model = JSON.parse(localStorage.getItem(model.cid));break;
case 'delete': localStorage.removeItem(model.cid); model = {};break;
default: break;
}
console.log(method);
}
var model = new Backbone.Model({name:'旺财'});
model.save(); // -> create
// model.fetch(); -> read

(4)事件支持

Backbone.Events 是一个对象,拥有 on()/off()/trigger() 方法绑定、解绑、触发这个对象上的事件,也拥有 listenTo()/stopListening() 方法监听、解除监听别的对象上发生的事件。

Backbone 下的所有类的原型都通过拷贝继承的方式继承了这些方法,因此每个类的实例都能够使用这些方法获得事件的支持:

var model = new Backbone.Model({ age: 18 });
model.on('xxx', function(){
console.log( 'xxx is triggered!' );
});
model.trigger('xxx');

每个类内置的事件列表如下:

 

"add" (model, collection, options):当一个model(模型)被添加到一个collection(集合)时触发。

"remove" (model, collection, options):当一个model(模型)从一个collection(集合)中被删除时触发。

"reset" (collection, options):当该collection(集合)的全部内容已被替换时触发。 "sort" (collection, options):当该collection(集合)已被重新排序时触发。 "change" (model, options):当一个model(模型)的属性改变时触发。 "change:[attribute]" (model, value, options):当一个model(模型)的某个特定属性被更新时触发。 "destroy" (model, collection, options):当一个model(模型)被destroyed(销毁)时触发。 "request" (model_or_collection, xhr, options):当一个model(模型)或collection(集合)开始发送请求到服务器时触发。 "sync" (model_or_collection, resp, options):当一个model(模型)或collection(集合)成功同步到服务器时触发。 "error" (model_or_collection, resp, options):当一个model(模型)或collection(集合)的请求远程服务器失败时触发。 "invalid" (model, error, options):当model(模型)在客户端 validation(验证)失败时触发。 "route:[name]" (params):当一个特定route(路由)相匹配时通过路由器触发。 "route" (route, params):当任何一个route(路由)相匹配时通过路由器触发。 "route" (router, route, params):当任何一个route(路由)相匹配时通过history(历史记录)触发。 "all":所有事件发生都能触发这个特别的事件,第一个参数是触发事件的名称。

Backbone 对象中的很多方法都允许传递一个 options 参数。如果不希望方法的执行触发事件,可以传递 { silent: true } 选项:

var model = new Backbone.Model({ age: 19 });
model.on('change', function(){
alert('age changed!');
});
model.set('age', 20, { silent: true }); // 不触发 change 事件

Backbone.Model - 数据模型

Backbone.Model 用来封装一个 JSON 对象,通过 attributes 属性引用 JSON 对象:

var Man = Backbone.Model.extend({
defaults:{
name: 'PXM'
}
});
var man = new Man({ age: 18 });
man.set('sex', 'male');
console.log(man.attributes); // => Object {age: 18, name: "PXM", sex: "male"}

但是应该避免使用 model.attributes.name 的方式直接访问封装的 JSON 对象 。应该使用 set() 和 get() 方法设置和获取 JSON 对象的属性,通过这种方式使得 Backbone 能够监控 JSON 对象的变化:

var model = new Backbone.Model();
model.on('change:name', function(model,val){
console.log('val:' + val);
});
model.set('age', '18');
model.set('name', 'PXM'); // val:PXM

Backbone.Model 可以很方便的对 JSON 对象的属性值进行校验:

var MyModel = Backbone.Model.extend({
initialize: function(){
this.bind("invalid",function(model,error){
alert(error);
});
},
validate: function(attributes) {
if (attributes.age < 0) {
return "年龄不能小于0";
}
}
}); var model = new MyModel({name: 'PXM'});
model.set('age', '-1', {validate: true}); // set()方法默认不校验,只有指定validate选项为true时才会校验
model.save(); // save()方法会自动进行校验

Backbone.Collection - 集合

Backbone.Collection 内部封装了一个数组,通过 models 引用这个数组,这个数组中保存着添加进该集合的所有数据模型实例:

var collection = new Backbone.Collection([{name: '旺财', age: 8}, {anme: '来福', age: 18}]);
console.log(collection.models); // [B…e.Model, B…e.Model]

与 Backbobe.Model 类似,应该避免使用 collection.models[0] 的方式直接操作内部封装地数组。应该使用 get()/add()/remove()/set() 等方法设置和获取数组的元素,使得 Backbone 能够监控数组的变化:

var collection = new Backbone.Collection();
collection.on('add', function(){
console.log('集合中添加了新的模型...');
});
collection.add({name: '旺财', age: 8}); // 集合中添加了新的模型...

如果集合实例拥有 comparator 实例方法,则在进行 add()/remove()/set() 操作结束之后会自动调用 sort() 方法进行一次排序,并且触发一次 sort 事件:

var Girl = Backbone.Model.extend();
var Girls = Backbone.Collection.extend({
model: Girl,
comparator: function(model1, model2) {
var age1 = model1.get('age');
var age2 = model2.get('age');
if (age1 > age2) {
return 1;
} else if (age1 === age2) {
return 0;
} else {
return -1;
}
}
}); var girls = new Girls(); girls.on('sort', function(){
console.log('集合重新排序了...');
}); girls.add([{ // 集合重新排序了...
name: '小黄',
age: 20
}, {
name: '小红',
age: 18
}, {
name: '小兰',
age: 22
}]); console.log(girls.pluck('name')); // ["小红", "小黄", "小兰"]

Backbone.Router - 路由

Backbone.Router 用来提供路径与 action 的映射关系。路径与路由字符串(或者正则表达式)去匹配,匹配上则调用对应的 action。

路由字符串中包含以下规则:

  • :param,匹配到下个/之间的字符
  • *splat,匹配到路径最后之间的所有字符
  • (),表示括号内的字符是可选的

Backbone.Router 实例使用 route() 方法来注册路由字符串与action的映射关系:

var route1 = new Backbone.Router().route('(/)', 'index', function(){  // route 方法是另一种绑定路由 Action映射关系的方法
console.log('index');
});

在 Backbone.Router 的构造函数中,会遍历 routes 属性,依次调用 route() 方法对 routes 对象中的每一个映射关系进行注册:

var MyRouter = Backbone.Router.extend({
routes: {
"(/)": "index", // http://www.example.com -> index()
"help(/)": "help", // http://www.example.com/#/help -> help()
"search/:keyword(/p:page)(/)": "search", // http://www.example.com/#/search/baidu/p2 -> search('baidu', 2)
"download/*file(/)": "download", // http://www.example.com/#/download/aa/bb.txt -> download('aa/bb.txt')
"*error": "error" // http://www.example.com/#/xxx -> fourOfour('xxx')
},
index: function(){
console.log('index');
},
help: function(){
console.log('help');
},
search: function(keyword, page){
console.log('search', keyword, page);
},
download: function(file){
console.log('download', file);
},
error: function(error) {
console.log('error', error);
}
});
var router = new MyRouter();

Backbone.Router 实例可以绑定 route:name 事件(其中的 name 是 Action 函数的名字),该事件在 Action 被调用时触发:

router.on('route:index', function(){
alert('进入index了');
});

Backbone.History - 历史管理和导航

Backbone.history 是 Backbone 全局路由服务,用来监听页面状态的变化,并且触发相应的路由。

Backbone.history 的 handlers 属性引用一个数组,该数组里保存着在当前页面创建的所有的 Backbone.Router 实例中注册的所有路由和 action 的映射关系:

var route1 = new Backbone.Router().route('index', 'index', function(){  // route 方法是另一种绑定路由 Action映射关系的方法
console.log('index');
});
var route2 = new Backbone.Router().route('help', 'help', function(){
console.log('help');
});
Array.prototype.forEach.call(Backbone.history.handlers, function(n,i){
console.log(n.route);
});
// /^help(?:\?([\s\S]*))?$/
// /^index(?:\?([\s\S]*))?$/

Backbone.history 监控页面状态变化的机制是:

  • 浏览器支持pushState,Backbone 将监视 popstate 事件来捕捉页面状态的变化
  • 浏览器不支持 pushState,Backbone 将监视 onhashchange 事件以捕捉页面状态的变化
  • 浏览器不支持 onhashchange 事件,Backbone 将使用轮询技术定时检测 url 的变化

当页面上所有的 Backbone.Router 实例创建好之后,需要手动调用Backbone.history.start();去启动 Backbone 对当前页面状态的监听。可以在选项中指定使用 pushState 方式还是 hashChange 方式。

Backbone.history.start({
pushState : true, // 使用 pushState 方法导航,Backbone 将监控 pathname 中相对于 root 路径的相对路径的变化
hashChange: false, // 使用 hashChange 方式导航,Backbone 将监控 hash 的变化
root: "/public/search/" // 应用的根路径,默认 /
});

  

Backbone 对页面状态的监听开启之后,除了用户响应用户操作的被动导航,在应用中还可以调用 router.navigate() 方法主动导航到新的视图。使用该方法默认不会触发路由,并且会保存到浏览器历史记录。可以使用选项来控制这些行为:

router.navigate('search/baidu/p2', {
trigger: true, // 触发路由
replace: true // 不被浏览器记录
});

Backbone.View - 视图

Backbone.View 绑定到一个 DOM 元素上,该视图下所有的事件、渲染全部限制在该 DOM 元素内部进行。Backbone.View 使用 el 属性指定一个选择器表达式:

var view = new Backbone.View({
el: 'body'
});

每个 Backbone.View 实例有一个 render() 方法,默认的 render() 方法没有任何的操作。创建一个新的视图实例时,需要重写 render() 方法。在 render() 方法中可以直接操作 DOM 元素来更新视图,也可以使用各种 js 模板渲染引擎来渲染视图。

使用 render() 方法来更新视图的好处是:过绑定视图的 render 函数到模型的 "change" 事件 — 模型数据会即时的显示在 UI 中:

var MyView = Backbone.View.extend({
initialize: function(){
this.listenTo(this.model, "change", this.render);
},
render: function(){
this.$el.html('Hello,'+this.model.get('name'));
}
}); var view = new MyView({
el: 'body',
model: new Backbone.Model({name: 'PXM'})
}); view.render();
// view.model.set('name', 'XXX');

Backbone 不强制依赖 jQuery/Zepto,但是 Backbone.View 的 DOM 操作依赖于 jQuery/Zepto 等类库,因此在使用 Backbone.View 时如果没有引入 jQuery/Zepto 会报错。

每个 Backbone.View 实例拥有一个 $el 属性引用一个视图元素的缓存 jQuery 对象。拥有一个 $ 属性引用一个函数function(selector){ return this.$el.find(selector); }, 该函数用于在视图内部获取元素。

var MyView = Backbone.View.extend({
initialize: function(){
this.$('.red').css('color', 'red'); // this.$('.red') 只获取到本视图内的 .red 元素
}
});
var view = new MyView({
el: '#view1'
});

Backbone 使用 jQuery/Zepto 为视图内的 DOM 元素绑定事件。由于视图内的元素是动态变化的,因此需要在视图元素上代理绑定事件。Backbone.View 使用 delegateEvents() 方法进行代理事件绑定:

var MyView = Backbone.View.extend({
initialize: function(){
this.render();
this.listenTo( this.model, 'change', this.render );
},
render: function(){
this.$el.html('Hello,<span>' + this.model.get('name') + "</span>");
}
});
var view = new MyView({
el: '#view1',
model: new Backbone.Model({'name': 'PXM'})
}); view.delegateEvents({"click span": function(){
var name = prompt('请输入新的名字:');
this.model.set('name', name || '');
}});

Backbone.View 在实例化时,会对 events 属性内部的事件自动调用 delegateEvents() 进行代理事件绑定:

var MyView = Backbone.View.extend({
initialize: function() {
this.render();
this.listenTo(this.model, 'change', this.render);
},
render: function() {
this.$el.html('Hello,<span>' + this.model.get('name') + "</span>");
},
events: {
'click span': function() {
var name = prompt('请输入新的名字:');
this.model.set('name', name || '');
}
}
});
var view = new MyView({
el: '#view1',
model: new Backbone.Model({
'name': 'PXM'
})
});

源码分析

 //     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 (function (root, factory) { // AMD 模块化规范兼容
// 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, _, $);
});
// CMD 模块化规范兼容
// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {
var _ = require('underscore');
factory(root, exports, _);
// 浏览器全局引用
// Finally, as a browser global.
} else {
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.
var previousBackbone = root.Backbone; // Create local references to array methods we'll want to use later.
var array = [];
var slice = array.slice; // Current version of the library. Keep in sync with `package.json`.
Backbone.VERSION = '1.1.2'; // 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; // 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');
//
var Events = Backbone.Events = { // Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on: function (name, callback, context) {
// 如果 name 是JSON形式的,那么第二个参数 callback 实际上是 context
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
/*
this._events 结构:
{
'change': [ { callback: ..., context: ..., ctx: ... }, ... ],
'add': [...]
}
*/
this._events || (this._events = {});
var events = this._events[name] || (this._events[name] = []);
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: function (name, callback, context) {
if (!eventsApi(this, 'once', name, [callback, context]) || !callback) return this;
var self = this;
// 使用 once 代理绑定的事件回调,执行之前先解绑
var once = _.once(function () {
self.off(name, once);
callback.apply(this, arguments);
});
// 代理事件回调中使用 _callback 引用原本的回调,这个将作为 解绑 once() 方法绑定的回调的依据
once._callback = callback;
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: function (name, callback, context) {
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this; // xxx.off(); -> 解绑所有事件
// Remove all callbacks for all events.
if (!name && !callback && !context) {
this._events = void 0;
return this;
} // 传递了参数 name,直接在 _event.name 数组中找需要解绑的回调函数,然后删除
// 没有传递参数 name,需要在 _evnet 下所有的所有的数组中找需要解绑的回调函数,然后删除
var names = name ? [name] : _.keys(this._events);
for (var i = 0, length = names.length; i < length; i++) {
name = names[i]; // Bail out if there are no events stored.
var events = this._events[name];
if (!events) continue; // xxx.off('name') -> 解绑 name 事件下所有的事件回调
// Remove all callbacks for this event.
if (!callback && !context) {
delete this._events[name];
continue;
} // Find any remaining events.
var remaining = [];
for (var j = 0, k = events.length; j < k; j++) {
var event = events[j];
if (
callback && callback !== event.callback &&
callback !== event.callback._callback ||
context && context !== event.context
) {
// _event[name] 数组中与传递参数中的回调不匹配的回调 push 进 remaining 数组
remaining.push(event);
}
} // 重新赋值 _event.name 数组。为什么不直接使用 splice() 方法删除匹配的回调?
// 可能他觉得直接 splice() 效率太低。。。
// Replace events if there are any remaining. Otherwise, clean up.
if (remaining.length) {
this._events[name] = remaining;
} else {
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).
trigger: function (name) {
if (!this._events) return this;
// args -> 除去 name 参数,剩下的参数集合
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
// all 也是一个事件名
var allEvents = this._events.all;
// 触发 非all 事件时,第一个参数 不是 name
if (events) triggerEvents(events, args);
// 触发 all 事件时第一个参数是 name
if (allEvents) triggerEvents(allEvents, arguments);
return this;
}, // 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.
listenTo: function (obj, name, callback) {
// this.__listeningTo 数组保存着 被监听的对象,使用 stopListening() 方法可以解除被监听对象上的事件绑定
var listeningTo = this._listeningTo || (this._listeningTo = {});
var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
listeningTo[id] = obj;
// name 如果是 JSON 的形式,callback 参数实际上是 context,因此设置为 this
if (!callback && typeof name === 'object') callback = this;
// 事件绑定到 被监听对象
obj.on(name, callback, this);
return this;
}, listenToOnce: function (obj, name, callback) {
/* JSON的形式
xxx.listenToOnce( obj, {
change: function(){ ... },
add: function(){ ... }
} );
*/
if (typeof name === 'object') {
for (var event in name) this.listenToOnce(obj, event, name[event]);
return this;
}
/* name 空格分隔的形式
xxx.listenToOnce( obj, 'change add', function(){ ... } )
*/
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
this.listenToOnce(obj, names[i], callback);
}
return this;
}
if (!callback) return this;
// 使用 once 代理,执行之前先停止监听
var once = _.once(function () {
this.stopListening(obj, name, once);
callback.apply(this, arguments);
});
once._callback = callback;
return this.listenTo(obj, name, once);
}, // Tell this object to stop listening to either specific events ... or
// to every object it's currently listening to.
stopListening: function (obj, name, callback) {
var listeningTo = this._listeningTo;
if (!listeningTo) return this;
// stopListening(obj) -> 在listeningTo列表中 删除被监控对象
var remove = !name && !callback;
// name 如果是 JSON 的形式,callback 参数实际上是 context,因此设置为 this
if (!callback && typeof name === 'object') callback = this;
// listeningTo( obj, name, callback ) -> 只解除监听指定对象上的事件
if (obj)(listeningTo = {})[obj._listenId] = obj;
// listeningTo(null, name, callback ) -> 解除 被监听对象列表 _listeningTo 中所有被监听对象上的 事件
for (var id in listeningTo) {
obj = listeningTo[id];
obj.off(name, callback, this);
// stopListening(obj) -> 在listeningTo列表中 删除被监控对象
if (remove || _.isEmpty(obj._events)) delete this._listeningTo[id];
}
return this;
} }; // Regular expression used to split event strings.
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.
var eventsApi = function (obj, action, name, rest) {
if (!name) return true; /* JSON 形式
model.on({
'change:name': function(){
console.log('change:name fired');
},
'change:age': function(){
console.log('change:age fired');
}
});
*/
// Handle event maps.
if (typeof name === 'object') {
for (var key in name) {
// obj.on( name, callback, context )
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
} /* 空格分割形式:
model.on( 'change:name change:age', function(){
console.log( 'change:name or change:age fired' );
} );
*/
// Handle space separated event names.
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, length = names.length; i < length; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
} return true;
}; // 理解不了,难道使用call比apply更快???
// 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.
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.
var Model = Backbone.Model = function (attributes, options) {
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId('c'); // 每个Model实例获得一个唯一标识
this.attributes = {};
// options.collecton 的作用???
if (options.collection) this.collection = options.collection;
// option.parse 的作用???
if (options.parse) attrs = this.parse(attrs, options) || {};
// 合并 实例属性 defaults 的值或者 实例方法 defaults 的执行结果
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
// 构造函数中是调用 set() 方法进行设值, set() 方法是循环拷贝 attr 中的每一个属性,因此避免在外部修改 attr 对象对模型数据产生的影响
this.set(attrs, options);
// 用来保存上次 change 事件触发时 改变的属性集合
this.changed = {};
// 调用实例的 initialize 方法完成一些初始化工作
this.initialize.apply(this, arguments);
}; // Events -> Model.prototype -- 拷贝继承
// Model.prototype -> model -- 原型继承
// => 结果是 model 拥有了 Events 对象中的 on/off/listenTo 等方法
// Attach all inheritable methods to the Model prototype.
_.extend(Model.prototype, Events, { // 上次 change 事件发生时,修改的属性集合
// A hash of attributes whose current and previous value differ.
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) {
// 这里返回了一个 model 内封装地 pojo 对象的一个拷贝,但是需要注意:_.clone()方法不会进行深拷贝,因此当 pojo 对象的某些属性值 是 对象时,在外部对这个对象进行修改可能会影响到 pojo
return _.clone(this.attributes);
}, // 同步方法,默认使用全局的 Backbone.sync 方法,Backbone.sync 方法默认调用 $.ajax 发送 REST 风格的HTTP请求
// 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) {
// get() 方法如果返回的是一个对象,可能导致 pojo 在外部被更改
return this.attributes[attr];
}, // 获取属性并进行 escape() 转移
// 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;
}, // Special-cased proxy to underscore's `_.matches` method.
matches: function (attrs) {
// _.matches/matcher() 接受一个json返回一个函数,这个函数用于判断对象是否和json中的键值对匹配
return _.matches(attrs)(this.attributes);
}, // 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.
// options 中主要有两个:
// 1: silent - 不触发 change 事件
// 2: unset - 参数中的属性都会在 attributes 中删除
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;
// JSON形式的参数,第二个参数是 options
options = val;
} else {
(attrs = {})[key] = val;
} options || (options = {}); // 校验
// Run validation.
// this._validate() 返回 false, 则直接返回false,不执行后面代码
if (!this._validate(attrs, options)) return false; // Extract attributes and options.
unset = options.unset;
silent = options.silent; // changes 用来保存本次 set() 操作需要更新的字段名
changes = []; // 保存原来的 _changing 值
changing = this._changing;
// 开始设值时:this._changing 值设置为 true
this._changing = true; // 如果不是 “正在改变中”,克隆一份 attributes
// 如果是 “正在改变中”,这种情况下是在 change 事件处理函数中调用了 set() 方法,只能算同一个过程,因此不能拷贝一个全新的 attributes,而是继续使用原来的 _previousAttributes
if (!changing) {
this._previousAttributes = _.clone(this.attributes);
this.changed = {};
}
// current 引用当前的 attributes, prev引用当前 attributes 的一个克隆
// 第一次调用 set() 进来,current 和 prev 值相同
// 在 change 事件中调用 set() 进来,由于 current 中的部分属性可能已经改变。而 prev 值还是外层 set() 调用之前的属性值
current = this.attributes, prev = this._previousAttributes; // 如果set()中改变id属性,则模型的 id 也更新
// 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];
// 需要更新的字段名进入 changes 数组
if (!_.isEqual(current[attr], val)) changes.push(attr);
// this.changed -> 保存着已经修改的属性
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
// 如果外层 set() 修改了一个属性值,内层 set() 又改为了原值,则整个设值过程中,attr并没有改变,所以要删除
delete this.changed[attr];
}
// 设置了 unset 选项,则 set() 参数中的属性都会被删除
unset ? delete current[attr] : current[attr] = val;
}
// silent 选项可以避免触发事件
// 触发 change:name 事件
// Trigger all relevant attribute changes.
if (!silent) {
if (changes.length) this._pending = options;
for (var i = 0, length = changes.length; i < length; i++) {
// 第一个参数是 model,第二参数是 改变之后的值
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.
// 注意:在 change 事件处理函数中嵌套调用 set() 方法,则直接返回,不会触发 change 事件。只有在最外层 set() 方法中才会触发 change 事件。
if (changing) return this;
if (!silent) {
// 使用 while 循环,是因为 可能在 change 事件处理程序中调用 set() 方法。由于嵌套 set() 中不会触发 change 事件,因此需要在这里触发多次
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
// 结束设值时:this._changing 值设置为 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) {
// this.changed 保存着自上次 change 事件触发时修改的属性值。
// 之所以说 是上次 change 事件触发而不是上次调用 set() 方法,是因为在 change:name 事件中调用的set()方法中不回更新 this.changed 对象
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) {
// 没有传 diff 参数,则直接返回 this.changed
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) {
// 传了 diff 参数,则返回与 diff 对象中与 _previousAttributes 不同的属性
if (_.isEqual(old[attr], (val = diff[attr]))) continue;
(changed || (changed = {}))[attr] = val;
}
return changed;
}, // 获取之前的某个属性值,不过当 attr 参数为空时,返回整个 _previousAttributes 对象
// 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];
}, // 获取之前的整个 _previousAttributes 对象
// 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) 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.stopListening();
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) {
// validate 选项不为true,则跳过检验,直接返回true
// this.validate 是实例的 validate 方法
if (!options.validate || !this.validate) return true;
attrs = _.extend({}, this.attributes, attrs);
// 如果 this.validate 方法中校验失败,则需要返回一个值
var error = this.validationError = this.validate(attrs, options) || null;
// 判断 this.validate() 的返回值,返回值为空则校验成功
if (!error) return true;
// 否则校验失败,触发 invalid 事件(注意:invalid:name 事件不存在)
this.trigger('invalid', this, error, _.extend(options, {
validationError: error
}));
return false;
} }); // 这写都是 underscore 的对象相关方法:
// keys 获取对象所有可枚举属性的集合
// values 获取对象所有可枚举属性值的集合
// pairs 将对象转化成为 [key, value] 形式的数组
// invert 将对象的 key 和 value 对换,必须保证 value 值唯一
// pick 将对象中的指定 key 的属性选取出来作为一个新的对象
// omit 与pick相反,将对象中的除指定 key 以外的属性选取出来作为一个新的对象
// chain 返回一个封装的对象. 在封装的对象上调用方法会返回封装的对象本身, 直道 value 方法调用为止.
// isEmpty 判断对象是否不包含任何属性
// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit', 'chain', 'isEmpty']; // Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function (method) {
if (!_[method]) return;
Model.prototype[method] = function () {
// arguments 是一个类数组对象,通过 slice() 方法转变我真的数组
var args = slice.call(arguments);
// 将 this.attributes 插入最前面
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 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.
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, { // model 属性引用该 Collection 存储的模型类型的构造函数,默认是 Model,可以通过 extend 方法扩展 Model 类
// 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) {
// 调用数组中各个model的toJSON()方法转换成JSON
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) {
/*
options = {
merge: false
add: true,
remove: false
}
*/
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 = {});
for (var i = 0, length = models.length; i < length; i++) {
// 根据 id 获取 collection.models 中的 model
var model = models[i] = this.get(models[i]);
// 不存在 model则不进行任何操作
if (!model) continue;
// 删除 _byId 索引中的记录
var id = this.modelId(model.attributes);
if (id != null) delete this._byId[id];
delete this._byId[model.cid];
// 删除 collection.models 中的 model
var index = this.indexOf(model);
this.models.splice(index, 1);
this.length--;
// 触发 model 的 remove 事件
if (!options.silent) {
options.index = index;
model.trigger('remove', model, this, options);
}
// 删除 model 与 collection 的关联
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] : []) : models.slice();
var id, model, attrs, existing, sort;
var at = options.at;
if (at != null) at = +at;
if (at < 0) at += this.length + 1;
var sortable = this.comparator && (at == null) && options.sort !== false;
// this,comparator 可以是字符串
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;
var orderChanged = false; // Turn bare objects into model references, and prevent invalid models
// from being added.
// 遍历需要处理的模型列表
for (var i = 0, length = models.length; i < length; i++) {
attrs = models[i]; // If a duplicate is found, prevent it from being added and
// optionally merge it into the existing model.
// get() 方法根据 this._byId对象返回对应的 model,该集合没有该 model 则返回 undefined
if (existing = this.get(attrs)) {
if (remove) modelMap[existing.cid] = true;
if (merge && attrs !== existing) {
attrs = this._isModel(attrs) ? attrs.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) {
// _prepareModel() 接受一个 模型对象或者模型属性对象,返回一个模型对象
// -> 在这里,model.collection 指向 this,模型和集合发生关系
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
// 将 model 推进 toAdd数组
toAdd.push(model);
// 通过_byId[cid/id] 可以索引到model,并且对 model 绑定事件
this._addReference(model, options);
} // Do not add multiple models with the same `id`.
// model 要么是通过 get() 方法获取的 ,要么是通过 _prepareModel() 方法生成的 model
model = existing || model;
if (!model) continue;
id = this.modelId(model.attributes);
if (order && (model.isNew() || !modelMap[id])) {
order.push(model); // Check to see if this is actually a new model at this index.
orderChanged = orderChanged || !this.models[i] || model.cid !== this.models[i].cid;
} modelMap[id] = true;
} // Remove nonexistent models if appropriate.
if (remove) {
for (var i = 0, length = this.length; i < length; 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 || orderChanged) {
// 在添加进集合之后进行排序
if (sortable) sort = true;
this.length += toAdd.length;
if (at != null) {
// at 不为空时,将 toAdd 数组插入到 this.models 数组的指定位置处
for (var i = 0, length = toAdd.length; i < length; i++) {
this.models.splice(at + i, 0, toAdd[i]);
}
} else {
// 否则,将 toAdd 插入到 this.models 数组的后面
// order - 清空 this.models
if (order) this.models.length = 0;
var orderedModels = order || toAdd;
for (var i = 0, length = orderedModels.length; i < length; i++) {
this.models.push(orderedModels[i]);
}
}
} // Silently sort the collection if appropriate.
if (sort) this.sort({
// sort() 方法中不触发 sort 事件,则后面代码中 触发了 add 事件之后再触发 sort 事件
silent: true
}); // Unless silenced, it's time to fire all appropriate add/sort events.
// silence 不为 true,则需要触发每个 model 的 add 事件
if (!options.silent) {
var addOpts = at != null ? _.clone(options) : options;
for (var i = 0, length = toAdd.length; i < length; i++) {
if (at != null) addOpts.index = at + i;
// 在事件处理函数中可以是用 addOpts.indes 新增的模型在 数组中 的索引
// add 事件是模型的
(model = toAdd[i]).trigger('add', model, this, addOpts);
}
// 如果排序了,需要触发 collection 的 sort 事件
if (sort || orderChanged) 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 ? _.clone(options) : {};
for (var i = 0, length = this.models.length; i < length; i++) {
// 删除每个 model.collection,解除每个 model 上的事件绑定
this._removeReference(this.models[i], options);
}
// options.previousModels 保存reset之前的model集合
options.previousModels = this.models;
// 清空 models
this._reset();
// 调用 add() 方法将 models 添加进 collection
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: 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;
// 获取id
var id = this.modelId(this._isModel(obj) ? obj.attributes : obj);
// obj 可以是 model 的 id / cid / model / model的attr
return this._byId[obj] || this._byId[id] || this._byId[obj.cid];
}, // Get the model at the given index.
at: function (index) {
// 负数的支持,coll.at(-1) 表示倒数第一个元素,即最后一个元素
if (index < 0) index += this.length;
return this.models[index];
}, // Return models with matching attributes. Useful for simple cases of
// `filter`.
// 返回与指定 attr 属性值匹配的第一个 model 或者 所有 model 组成的集合
where: function (attrs, first) {
var matches = _.matches(attrs);
return this[first ? 'find' : 'filter'](function (model) {
return matches(model.attributes);
});
}, // Return the first model with matching attributes. Useful for simple cases
// of `find`.
// 返回与指定 attr 属性匹配的第一个 model
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.models 中的数组进行一次排序,触发 sort 事件
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) {
// 如果是字符串,则调用 _.sortBy() 方法按照指定的属性进行排序
this.models = this.sortBy(this.comparator, this);
} else {
// 如果 this.comparator 是函数,直接调数组的原生 sort() 方法进行排序
this.models.sort(_.bind(this.comparator, this));
}
// 触发 sort 事件
if (!options.silent) this.trigger('sort', this, options);
return this;
}, // Pluck an attribute from each model in the collection.
pluck: function (attr) {
// 对数组中的每个元素调用 get(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;
// 没有设置 options.wait 选项,则 直接添加进集合
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, {
model: this.model,
comparator: this.comparator
});
}, // Define how to uniquely identify models in the collection.
// 获取 模型属性对象 中的id
modelId: function (attrs) {
return attrs[this.model.prototype.idAttribute || 'id'];
}, // Private method to reset all internal state. Called when the collection
// is first initialized or reset.
_reset: function () {
// 清空 models
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 (this._isModel(attrs)) {
// attrs 本来就是一个模型对象,则将 model.collection 指向本对象,直接返回
if (!attrs.collection) attrs.collection = this;
return attrs;
}
options = options ? _.clone(options) : {};
options.collection = this;
// 调用 this.model 构造函数创建一个模型,并且 model.collection 指向本 collection
var model = new this.model(attrs, options);
// 如果 options 中设置了 validate: true,则 this.model -> set() -> _validate(),验证错误信息保存在 model.validationError 中
// 检验成功,则直接返回新创建的模型对象
if (!model.validationError) return model;
// 校验失败,则触发 collection 的 invalid 事件,并返回 false
this.trigger('invalid', this, model.validationError, options);
return false;
}, // Method for checking whether an object should be considered a model for
// the purposes of adding to the collection.
// 判断 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 是一个对象,key 是模型的 cid,value 是模型对象
this._byId[model.cid] = model;
// 获取 model 的 id
var id = this.modelId(model.attributes);
// 根据 id 属性也确保能够找到 model
if (id != null) this._byId[id] = model;
// 给模型对象绑定事件
model.on('all', this._onModelEvent, this);
}, // Internal method to sever a model's ties to a collection.
_removeReference: function (model, options) {
// 删除 model.collection,切断 model 与 collection 的联系
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 (event === 'change') {
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;
}
}
// 每个在 model 触发的 change 事件,model 所属的 collection 上也需要触发
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', 'partition']; // Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function (method) {
if (!_[method]) return;
Collection.prototype[method] = function () {
var args = slice.call(arguments);
// this.models 作为第一个参数
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) {
if (!_[method]) return;
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...
var View = Backbone.View = function (options) {
this.cid = _.uniqueId('view');
options || (options = {});
// this.viewOptions = options.viewOptions
_.extend(this, _.pick(options, viewOptions));
// _ensureElement() 方法中会调用 setElement() 方法,setElement() 方法中调用 delegateEvents() 方法绑定 events 中的事件
this._ensureElement();
this.initialize.apply(this, arguments);
}; // 代理事件的写法: '.red click',也可以设置成'::'分割: '.red::click' -> JPX就是这么做的
// Cached regex to split keys for `delegate`.
var delegateEventSplitter = /^(\S+)\s*(.*)$/; // 视图选项
// List of view options to be merged 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.
// 使用 view.$(selector) 获取的是视图内的 dom 节点
$: 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 () {
// 卸载 el 节点
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.
_setElement: function (el) {
// 保存一个 jQuery 对象的 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`.
delegateEvents: function (events) {
// _result(this, 'events') -> 如果 this.events 是一个函数,则执行这个函数并返回结果;如果 this.events 不是一个函数,直接返回 this.events
if (!(events || (events = _.result(this, 'events')))) return this;
// 绑定之前 解绑所有事件,注意:Backbone 的所有事件都是批量一次性绑定完成的,不能一个一个的绑
this.undelegateEvents();
for (var key in events) {
var method = events[key];
// callback 也可以是 view 实例的一个方法名
if (!_.isFunction(method)) method = this[events[key]];
if (!method) continue;
// 分割 events 名, match[1] 为被代理元素,match[2] 为事件名
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) {
// 直接使用 jQuery 的事件绑定方法,并且事件命名在 .delegateEvents + cid 命名空间下
this.$el.on(eventName + '.delegateEvents' + this.cid, selector, listener);
}, // 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 () {
// 删除 .delegateEvents + cid 命名空间下绑定的所有事件
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.
undelegate: function (eventName, selector, listener) {
// 删除 .delegateEvents + cid 命名空间下 指定事件名,指定的 被代理元素,指定的 事件处理函数绑定
this.$el.off(eventName + '.delegateEvents' + this.cid, selector, listener);
}, // Produces a DOM element to be assigned to your view. Exposed for
// subclasses using an alternative DOM manipulation API.
_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) {
// 没有传递 el 属性,则 根据 id className tagName 等属性创建一个新的 dom 节点作为视图节点
var attrs = _.extend({}, _.result(this, 'attributes'));
if (this.id) attrs.id = _.result(this, 'id');
if (this.className) attrs['class'] = _.result(this, 'className');
// $el 和 el 保存创建的新节点的引用
this.setElement(this._createElement(_.result(this, 'tagName')));
this._setAttributes(attrs);
} else {
// 保存 $el 和 el 的引用
this.setElement(_.result(this, 'el'));
}
}, // Set attributes from a hash on this view's element. Exposed for
// subclasses using an alternative DOM manipulation API.
_setAttributes: function (attributes) {
// attributes 属性,常用的有 id className tagName 等字段
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.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';
// 直接转化成 JSON 字符串
params.data = JSON.stringify(options.attrs || model.toJSON(options));
} // For older servers, emulate JSON by encoding the request into an HTML-form.
// 老的服务器,不支持 application/json 形式的 contentType,可以使用 表单数据 application/x-www-form-urlencoded 来模拟。
// 表单数据提交有两种类型 encType:
// 普通控件使用 application/x-www-form-urlencoded 类型提交
// 带file控件的表单数据使用 multipart/form-data 类型提交
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.
// 老的服务器,不支持 PUT/DELETE/PATCH 类型的请求。则我们在发送的时候还是使用 POST 类型来发送请求,但是 data._method 属性中保存我们真实的 请求类型
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;
} // Pass along `textStatus` and `errorThrown` from jQuery.
var error = options.error;
options.error = function (xhr, textStatus, errorThrown) {
options.textStatus = textStatus;
options.errorThrown = errorThrown;
if (error) error.apply(this, arguments);
}; // Make the request, allowing the user to override any Ajax options.
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
// 触发模型的 request 事件
model.trigger('request', model, xhr, options);
return xhr;
}; // 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 () {
// 调用 jQuery 的 ajax() 方法
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;
// 根据 routes 对象,对其中的每一个映射使用 route 方法进行绑定
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) {
// 如果 route 不是一个正则表达式而是Backbone的路径映射字符串,则需要转化成正则表达式
if (!_.isRegExp(route)) route = this._routeToRegExp(route);
// name 的用途是在绑定 route:name 事件时有用,不考虑绑定事件,则可以省略 name 参数
// eg: this.route('(/)', function(){ alert('route') })
if (_.isFunction(name)) {
callback = name;
name = '';
}
// callback 没传,则默认 this.name 作为回调
// eg: this.route('(/)', 'index');
if (!callback) callback = this[name];
var router = this;
Backbone.history.route(route, function (fragment) {
var args = router._extractParameters(route, fragment);
// 在 callback 中返回false导致
if (router.execute(callback, args, name) !== false) {
// 触发 router 的 route:name 事件
router.trigger.apply(router, ['route:' + name].concat(args));
// 触发 router 的 route 事件
router.trigger('route', name, args);
// 触发 Backbone.history 的 route 事件
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) {
// 调用 history 对象的 navigate() 方法
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 通常是一个 路径 映射对象
this.routes = _.result(this, 'routes');
var route, routes = _.keys(this.routes);
while ((route = routes.pop()) != null) {
// route 是映射串,toutes[route] 是对应的 Action 函数
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.
// 抽取参数,给定一个 正则表达式route 和 URL 片段fragment
_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 () {
// 初始化 this.handlers 数组,这个数组用来保存所有的 router 实例 注册的 路径-回调关系
this.handlers = [];
// _.bindAll() 方法将 this.checkUrl 方法中的this固定死为this,相当于代理。这样可以确保在 this.checkUrl 作为事件处理函数时,this 依然能够指向 this.history 对象
_.bindAll(this, 'checkUrl'); // 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.
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(/[^\/]$/, '$&/');
// pathname 为 this.root 并且 ?search 字符串不为空
return path === this.root && !this.getSearch();
}, // 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 = decodeURI(this.location.pathname + this.getSearch());
var root = this.root.slice(0, -1);
if (!path.indexOf(root)) path = path.slice(root.length);
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._hasPushState || !this._wantsHashChange) {
// 如果 支持 pushState 并且没有设置 _wantsHashChange,则默认获取路径
fragment = this.getPath();
} else {
// 否则 获取 hash 值
fragment = this.getHash();
}
}
// 如果传递了 fragment 参数,则仅仅是滤出开头的 # / 或者空格
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: '/' // 当使用URL中的路径去匹配路由时,需要指定一个base/root url
}, this.options, options);
this.root = this.options.root;
this._wantsHashChange = this.options.hashChange !== false; // 是否想要监控 hash 的变化,默认 true -> 为true时Backbone抓取URL中的hash值去匹配
this._hasHashChange = 'onhashchange' in window; // 是否支持 onhashchange 事件
this._wantsPushState = !!this.options.pushState; // 是否想要监控 pushState 的变化,默认false -> 为true时Backbone抓取URL中的路径部分去匹配
// 初始化时,this.history 指向了 window.history
this._hasPushState = !!(this.options.pushState && this.history && this.history.pushState); // 是否支持pushState API
this.fragment = this.getFragment(); // 初始化 this.fragment,用这个值去和 current fragment 去比较来判断页面状态是否发生变化 // Normalize root to always include a leading and trailing slash.
// 在收尾加上'/',并且过滤去 this.root 开头或者结尾的多余'/',如 '///aa//' -> '/a/'
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...
// 浏览器不支持 pushState,但是现在有不在 根路径上:则需要将 当前的路径相对于root路径的相对路径转化为 hash值,然后重定向到 转化之后的新的 url 上
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...
// 浏览器支持 pushState 并且当前在 根路径上,需要把 hash 值转化到为相对于root路径的相对路径,导航到新的 url 中
} 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`.
// 当需要使用 hashChange,但是浏览器不支持 onhashchange 时,创建一个隐藏的 iframe
if (!this._hasHashChange && this._wantsHashChange && (!this._wantsPushState || !this._hasPushState)) {
var iframe = document.createElement('iframe');
iframe.src = 'javascript:0';
iframe.style.display = 'none';
iframe.tabIndex = -1;
var body = document.body;
// Using `appendChild` will throw on IE < 9 if the document is not ready.
this.iframe = body.insertBefore(iframe, body.firstChild).contentWindow;
this.iframe.document.open().close();
this.iframe.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._hasPushState) {
// 支持 pushState API,监听 popstate 事件
addEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
// 支持 hashChange 时,监听 hashchange 事件
addEventListener('hashchange', this.checkUrl, false);
} else if (this._wantsHashChange) {
// 否则,轮询检测
this._checkUrlInterval = setInterval(this.checkUrl, this.interval);
}
// 启动时不想触发 route 事件时不执行,否则执行 loadUrl() 方法,loadUrl() 方法中去匹配路由并且执行对应的 action
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.
// 解绑 popstate 事件或者 hanshchange 事件
if (this._hasPushState) {
removeEventListener('popstate', this.checkUrl, false);
} else if (this._wantsHashChange && this._hasHashChange && !this.iframe) {
removeEventListener('hashchange', this.checkUrl, false);
} // Clean up the iframe if necessary.
// 删除创建的 空的 iframe
if (this.iframe) {
document.body.removeChild(this.iframe.frameElement);
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 数组
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.
// 检测当前 URL 是否发生变化,没有发生变化则返回false
checkUrl: function (e) {
// 获取当前的 URL 片段,可能是hash值,也可能是路径,视配置情况而定
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);
}
// url 没有改变,则直接返回false
if (current === this.fragment) return false;
// 发生改变,若不支持 hashchange 事件使用 空的iframe 保持url时,本页面需要导航到 新的 url
if (this.iframe) this.navigate(current); // 不保存记录,因为改变 iframe 的url时已经记录已经进入了history。也不触发事件,因为事件在 navigate 时已经触发了
// 执行路由匹配
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`.
// 执行一次路由匹配,匹配不成功返回false,匹配成功返回true
loadUrl: function (fragment) {
fragment = this.fragment = this.getFragment(fragment);
// _.any() 方法最多只能与一个 route 匹配
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;
// 默认不触发 route 事件,但是如果 options.trigger 为 true,则触发 route 事件
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;
// fragment为空串或者只是查询字符串(?),则root去除末尾的'/'
if (fragment === '' || fragment.charAt(0) === '?') {
root = root.slice(0, -1) || '/';
}
var url = root + fragment; // Strip the hash and decode for matching.
// 去除 fragment 中的 #hash,并且加密
fragment = decodeURI(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._hasPushState) {
// 使用 HTML5 history API 进行导航
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))) {
// 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.
// open().close() 用于欺骗 IE7 及以下版本保存历史记录
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.
// 如果明确指定不使用 hashchange,则在不支持pushState 的浏览器上使用刷新页面的方式
} 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 引用一个 History 实例,Backbone 的整个生命周期内,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.
// 第一个参数是需要扩展的 实例属性,第二个参数是是需要扩展的 静态属性,并且返回一个子类
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.
if (protoProps && _.has(protoProps, 'constructor')) {
// 如果 protoProps 中含有 constructor 属性,则子类调用 传递的 constructor 进行初始化
child = protoProps.constructor;
} else {
// 否则,子类调用 父类的构造方法进行初始化
child = function () {
return parent.apply(this, arguments);
};
} // Add static properties to the constructor function, if supplied.
// 扩展静态属性
_.extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling
// `parent`'s constructor function.
// 如果直接使用 child.prototype = new parent() 会调用 父类 parent 的构造方法,因此需要一个 Surrogate 做中转
var Surrogate = function () {
this.constructor = child;
};
Surrogate.prototype = parent.prototype;
// child.prototype 继承 parent.prototype 的属性
child.prototype = new Surrogate; // Add prototype properties (instance properties) to the subclass,
// if supplied.
// 扩展实例属性
if (protoProps) _.extend(child.prototype, protoProps); // Set a convenience property in case the parent's prototype is needed
// later.
// 子类的 __super__ 属性引用父类的prototype
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.
var urlError = function () {
throw new Error('A "url" property or function must be specified');
}; // Wrap an optional error callback with a fallback error event.
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; }));

Backbone笔记(续)的更多相关文章

  1. React.js入门笔记(续):用React的方式来思考

    本文主要内容来自React官方文档中的"Thinking React"部分,总结算是又一篇笔记.主要介绍使用React开发组件的官方思路.代码内容经笔者改写为较熟悉的ES5语法. ...

  2. 【Stage3D学习笔记续】真正的3D世界(一):透视矩阵

    如果各位看官跟着我的学习笔记一路看过来的话,一定会吐槽我的,这都是什么3D啊?从头到尾整个都是在使用GPU绘制一堆2D图像而已,的确,之前我们一直使用正交矩阵利用GPU加速来实现2D世界的展示,算不上 ...

  3. 【Stage3D学习笔记续】山寨Starling(十二):总结和一些没提到的东西

    我们的山寨Starling到这里就告一段落了,不得不说这是一个非常优秀的2D框架,他的设计和架构为后来的许多框架都提供了很好的参考,比如现在正在崛起的Egret,我们的一番解读也只是窥见了Starli ...

  4. 【Stage3D学习笔记续】山寨Starling(十一):Touch事件体系

    我们的山寨Starling版本将会在这里停止更新了,主要还是由于时间比较有限,而且我们的山寨版本也很好的完成了他的任务“了解Starling的核心渲染”,接下来的Starling解析我们将会直接阅读S ...

  5. 【Stage3D学习笔记续】山寨Starling(十):高效游戏设计、纹理集和ATF

    我发布了经过批处理优化的v0.3版,点击下载:https://github.com/hammerc/hammerc-study-Stage3D/archive/v0.3.zip 先看看我们批处理优化后 ...

  6. 【Stage3D学习笔记续】山寨Starling(八):核心优化(批处理)的实现

    批处理是使GPU进行高效绘制的一种技术手段,也是整个渲染流程中最核心的技术,到目前为止我们并没有使用到这种技术手段,下面我们看看我们现在的渲染机制. 先想一想我们最开始是怎么向GPU绘制一幅图像的,可 ...

  7. 【Stage3D学习笔记续】山寨Starling(六):动画实现和测试

    我发布了一个版本v0.2,该版本是未优化版本,且没有添加Touch事件体系,但是由于是最基础且未优化的,所以可以通过参考代码快速的了解实现原理. 接下来的一段笔记开始进行渲染优化,我会把所有的目光都集 ...

  8. 【Stage3D学习笔记续】山寨Starling(四):渲染代码实现及测试程序

    本章会实现最核心的代码,所以涉及点会比较多,这里会发布一个版本,方便日后的回退查看. 点击下载:https://codeload.github.com/hammerc/hammerc-study-St ...

  9. 【Stage3D学习笔记续】山寨Starling(三):Starling核心渲染流程

    这篇文章我们剔除Starling的Touch事件体系和动画体系,专门来看看Starling中的渲染流程实现,以及其搭建的显示列表结构. 由于Starling是模仿Flash的原生显示列表,所以我们可以 ...

随机推荐

  1. SQL Server 2005 控制用户权限访问表

    转自:http://www.cnblogs.com/gaizai/archive/2011/07/14/2106617.html 一.需求 在管理数据库过程中,我们经常需要控制某个用户访问数据库的权限 ...

  2. 【winform 学习】C# 转换成JSON对象

    C#里面对json的处理有2种,JavaScriptSerializer和DataContractJsonSerializer. JavaScriptSerializer读出来就是key-value  ...

  3. MYSQL数据表操作语句

    1.查看某数据库中的表 SHOW [FULL] TABLES [FROM db_name] [LIKE 'pattern'] SHOW TABLES列举了给定数据库中的非TEMPORARY表.也可以使 ...

  4. VC++ 文件系统

    using namespace System; using namespace System::IO; void ShowHelpMsg(){ Console::WriteLine(L"本程 ...

  5. Polymer.js

    Polymer 1.0 教程 安装 bower install --save Polymer/polymer

  6. ajax 中$.each(json,function(index,item){ }); 中的2个参数表示什么意思?

    $.each(json,function(index,item)里面的index代表当前循环到第几个索引,item表示遍历后的当前对象,比如json数据为:[{"name":&qu ...

  7. WPA破解原理简要——无线网络破解续

    一.破解密码的基础 关于密码的破解我再重复一次好了.密码破解就两大类方法.一是字典解密,而是暴力解密. 暴力解密就是采用穷举的方法——你密码是怎么组成的呢?无非就是数字.字母.符号,暴力解密就是采用一 ...

  8. js 日期按年月日加减

    <script> function isleapyear(year) { if(parseInt(year)%4==0 && parseInt(year)%100!=0)r ...

  9. C#先执行一段sql等后台操作后再提示是否后续操作confrim

    应用场景:例如选择一个单据号打击打印后先去数据库检索是否有打打印过,如果有则提示,已打印,是否再打 如果没有则不提示,直接进行打印. 实现原理:多做一个隐藏按钮去实现打印功能,页面上的打印按钮则进行数 ...

  10. HTML DOM 实例-Document 对象

    使用 document.write() 向输出流写文本 <html><body><script type="text/javascript">d ...