Backbone源码阅读手记
Backbone.js是前端的MVC框架,它通过提供模型Models、集合Collection、视图Veiew赋予了Web应用程序分层结构。从源码中可以知道,Backbone主要分了以下几个模块:
(function(root) {
Backbone.Events //自定义事件机制
Backbone.Model //模型
Backbone.Collection //模型集合
Backbone.Router //路由配置器
Backbone.View //视图
Backbone.sync //向服务器同步数据方法
})(this)
自己主要阅读了Events、Model、Collection、sync这几个模块,所以对这几个模块进行介绍。
Events模块
//Backbone的事件对象
var Events = Backbone.Events = { //事件订阅函数
//name:事件名
//callback:事件回调函数对象
//context:事件上下文
on: function(name, callback, context) {
//eventsApi的作用请看下方eventsApi方法的注释
if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
//_events对象用于存储各个事件的回调函数对象列表
//_events对象中的属性名为事件名称,而属性值则为一个保护函数对象的对象数组
this._events || (this._events = {});
var events = this._events[name] || (this._events[name] = []);
//将包含回调函数的对象添加到指定事件的回调函数列表,即注册事件
events.push({callback: callback, context: context, ctx: context || this});
return this;
}, //取消事件订阅
off: function(name, callback, context) {
var retain, ev, events, names, i, l, j, k;
if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;
//当name,callback,context都没指定时,取消订阅所有事件
if (!name && !callback && !context) {
this._events = void 0;
return this;
} //未指定name时,则取所有的事件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];
if ((callback && callback !== ev.callback && callback !== ev.callback._callback) ||
(context && context !== ev.context)) {
//保留
retain.push(ev);
}
}
}
if (!retain.length) delete this._events[name];
}
} return this;
}, //触发事件
//name: 触发的事件名
trigger: function(name) {
if (!this._events) return this;
//参数列表,不包含name
var args = slice.call(arguments, 1);
if (!eventsApi(this, 'trigger', name, args)) return this;
var events = this._events[name];
var allEvents = this._events.all;
//触发事件,即调用相应的回调函数
if (events) triggerEvents(events, args);
//任何事件触发时,all事件都会被触发
if (allEvents) triggerEvents(allEvents, arguments);
return this;
} }; //该函数的主要作用:
//当指定事件名为object对象时,将object对象中key作为事件名
//将obejct中的value作为回调函数对象,然后递归调用on、off、trigger
//当指定的事件名为string,但包含空格时,将string按空格切割,再依次递归调用
//该函数需要对应的看它是如何被调用的,直接看是比较难明白的
//当时我就看了好久没明白,函数名取为eventsApi对我一点帮助也没用。。
var eventsApi = function(obj, action, name, rest) {
if (!name) return true; //当指定的事件名为object时
if (typeof name === 'object') {
for (var key in name) {
obj[action].apply(obj, [key, name[key]].concat(rest));
}
return false;
} // 当指定的事件名包含空格时
//eventSplitter = /\s+/;
if (eventSplitter.test(name)) {
var names = name.split(eventSplitter);
for (var i = 0, l = names.length; i < l; i++) {
obj[action].apply(obj, [names[i]].concat(rest));
}
return false;
} return true;
}; //调用事件回调函数的函数
//可能是为了性能问题,才使用了switch,而不是直接使用default中的代码
//但我不太明白,这样为什么会提高效率,希望高人解答
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);
}
};
上面的代码就为Events的核心部分,我们可以从中得知:
Model模块
在Backbone中Model是一个构造函数
var Model = Backbone.Model = function(attributes, options) {
var attrs = attributes || {};
options || (options = {});
this.cid = _.uniqueId('c');
//存储相应model所应具有的属性
this.attributes = {};
if (options.collection) this.collection = options.collection;
//解析attrs,默认直接返回attrs
if (options.parse) attrs = this.parse(attrs, options) || {};
attrs = _.defaults({}, attrs, _.result(this, 'defaults'));
//设置model相应的属性
this.set(attrs, options);
this.changed = {};
//用于初始化model的函数,需要使用者自己指定
this.initialize.apply(this, arguments);
};
随后我们可以看到Model函数的原型扩展,需要注意的是Events对象被拓展到了Model的原型中,这样model对象也就有了事件机制:
//将Events和指定对象扩展至Model的原型中
_.extend(Model.prototype, Events, { //该方法用于向服务端同步数据(增、删、改)
//该方法默认调用的是Backbone.sync方法(ajax)
//我们可以通过替换Backbone.sync来使用我们自己的sync方法,比如mongodb,这样backbone也能
//用于Node.js后端
sync: function() {
return Backbone.sync.apply(this, arguments);
}, //获取model的属性值
get: function(attr) {
return this.attributes[attr];
}, //设置model的属性,当属性值发生变化时,触发'change'事件
//该方法为Model的核心
set: function(key, val, options) {
var attr, attrs, unset, changes, silent, changing, prev, current;
if (key == null) return this; //让set方法可以这样调用set({key: value ....}, options);
if (typeof key === 'object') {
attrs = key;
options = val;
} else {
(attrs = {})[key] = val;
} options || (options = {}); //验证设置的属性是否符合要求,_validate方法内部将会调用validate方法
//validate方法需要model使用者自己指定
//当设置的属性不符合要求时,直接返回false
if (!this._validate(attrs, options)) return false; //表示应当删除属性,而不是设置属性
unset = options.unset;
//当silent为true时,不触发change事件
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; if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; //设置model属性值
for (attr in attrs) {
val = attrs[attr];
//当设置的值与当前的model对象的属性值不同时,将要设置的属性的key加入changes列表中
if (!_.isEqual(current[attr], val)) changes.push(attr);
//this.changed存储改变了的属性和其属性值
if (!_.isEqual(prev[attr], val)) {
this.changed[attr] = val;
} else {
delete this.changed[attr];
}
unset ? delete current[attr] : current[attr] = val;
} //触发change事件
if (!silent) {
if (changes.length) this._pending = options;
//触发'change:变更的属性名'事件
for (var i = 0, l = changes.length; i < l; i++) {
this.trigger('change:' + changes[i], this, current[changes[i]], options);
}
} if (changing) return this;
if (!silent) {
//触发'change'事件,这里使用while,是因为change事件也有可能会调用set方法
//所以需要递归的调用
while (this._pending) {
options = this._pending;
this._pending = false;
this.trigger('change', this, options);
}
}
this._pending = false;
this._changing = false;
return this;
}
});
在这里我列出来的3个方法:
首先是sync方法,它用于与服务器端同步,model中的create、update、fetch、destory方法都是通过调用它来跟服务端交付,backbone默认实现的sync就是通过ajax与服务端交付,所以我认为,如果我们将sync替换为直接与sqlite、mongodb交付,这样backbone的Model也能用于服务器端了。
第二个是get方法:这个方法很简单,获取model的属性值(model的属性值是存在于model.attributes对象中)
第三个是set方法:该方法是model的核心,它用于设置model的属性值,首先调用this.validate方法(使用者需指定)验证属性值是否符合业务要求,之后对属性值一一设置,对于改变了的属性值触发'change:key'事件(没有指定silent)。最后再触发change事件。
Collection模块

Collection是model对象的有序集合(你可以指定它的comparator属性来进行相应的排序),它内部维护了一个model数组,它提供了集合的增删改查、排序操作 ,也使用了sync方法与服务器端同步。Collection也将Events包含到了自身当中。
Collection中最强大方法就是set,你可以使用它进行增加、删除、修改:
set: function(models, options) {
//other code.....
var add = options.add, merge = options.merge, remove = options.remove;
var order = !sortable && add && remove ? [] : false;
//迭代参数models,对于其中每个model进行相应的操作
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 (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;
//否则对集合进行添加操作,记录下应该被添加的对象
} else if (add) {
model = models[i] = this._prepareModel(attrs, options);
if (!model) continue;
toAdd.push(model);
this._addReference(model, options);
}
if (order) order.push(existing || model);
}
//根据之前的记录下的应删除的对象,删除集合中相应的对象
if (remove) {
for (i = 0, l = this.length; i < l; ++i) {
if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model);
}
//删除集合中相应的对象,并触发remove事件
if (toRemove.length) this.remove(toRemove, options);
}
//进行添加操作
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]);
}
}
}
//当进行了添加或修改操作,并且可以排序时,则对集合进行排序
if (sort) this.sort({silent: true});
if (!options.silent) {
for (i = 0, l = toAdd.length; i < l; i++) {
//触发add事件
(model = toAdd[i]).trigger('add', model, this, options);
}
//触发排序事件
if (sort || (order && order.length)) this.trigger('sort', this, options);
}
return singular ? models[0] : models;
}
我列出了set方法中的大部分代码,它根据指定的参数进行添加 删除 修改操作,并进行排序。而我个人感觉这样不是太好,因为一个方法做了太多的事情,有点array.splice的味道,使得整个方法的代码十分冗长,也变得不易理解。。。我比较菜,看这个看了好久。。
Collection虽然提供了set,但它还是提供了add(内部调用set)、reset(内部调用set)、remove方法。Collection还跟Model一样提供了sync方法,用于和服务端同步数据。
最后,Collection还提供了underscore.js库对集合的操作方法,它们都是调用underscore库实现的方法。
Sync模块
sync是Backbone用于同步服务端数据的方法,它的默认实现:
Backbone.sync = function(method, model, options) {
var type = methodMap[method];
//some init code....
//默认使用json格式
var params = {type: type, dataType: 'json'};
if (!options.url) {
params.url = _.result(model, 'url') || urlError();
}
//将请求的数据类型设置为json
//将对象格式化为json数据
if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
params.contentType = 'application/json';
params.data = JSON.stringify(options.attrs || model.toJSON(options));
}
//some for old server code....
//and some for ie8 code
// ajax请求
var xhr = options.xhr = Backbone.ajax(_.extend(params, options));
model.trigger('request', model, xhr, options);
return xhr;
}
上面就是它的主要代码,我将一部分有关兼容性的代码给移除了。
阅读源码的收获:
因为自己接触js不久,就想去看看一个优秀的js项目是如何写的,所以就选择了backbone这个相对比较轻量级的框架。当然,因为自己水平有限,加上写的js代码也不多,不能很好领悟backbone的设计思想,也不能很好的指出backbone有什么不足的地方,但我还是有一些收获:
1.学到了js中的一些使用技巧,比如使用||操作符 model || model = {},还有如何利用参数在代码中实现类似重载的行为(js函数本身没有重载)
2.对this变量的绑定有了更好的理解
3.相对于c#而言,js是一门弱类型的动态语言,所以对一个对象的扩展要灵活多
4.在c#中,如果我需要去提高模块的可扩展性,我可能要利用接口利用多态去实现,但js则就轻松的多,我只需暴露一个属性接口即可,因为我可以轻松的替换他,就像Backbone.sync一样,但带来的缺点就是如果你的sync方法并不符合设计,你只会在运行时发现错误,而不是编译时
参考资料:https://github.com/jashkenas/backbone/blob/master/backbone.js
Backbone源码阅读手记的更多相关文章
- 【 js 基础 】【 源码学习 】backbone 源码阅读(一)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(二)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(source-code-study)进行参考交流,有详细的源码注释,以及知识总结,同时 ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(三)浅谈 REST 和 CRUD
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- 【 js 基础 】【 源码学习 】backbone 源码阅读(三)
最近看完了 backbone.js 的源码,这里对于源码的细节就不再赘述了,大家可以 star 我的源码阅读项目(https://github.com/JiayiLi/source-code-stud ...
- goroutine调度源码阅读笔记
以下为本人阅读goroutine调度源码随手记的笔记,现在还是一个个知识点的形式,暂时还没整理,先发到这里,一点点更新: 1). runq [256]guintptr P 的runable队列最大 ...
- 【原】FMDB源码阅读(三)
[原]FMDB源码阅读(三) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 FMDB比较优秀的地方就在于对多线程的处理.所以这一篇主要是研究FMDB的多线程处理的实现.而 ...
- 【原】FMDB源码阅读(二)
[原]FMDB源码阅读(二) 本文转载请注明出处 -- polobymulberry-博客园 1. 前言 上一篇只是简单地过了一下FMDB一个简单例子的基本流程,并没有涉及到FMDB的所有方方面面,比 ...
- 【原】FMDB源码阅读(一)
[原]FMDB源码阅读(一) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 说实话,之前的SDWebImage和AFNetworking这两个组件我还是使用过的,但是对于 ...
- 【原】AFNetworking源码阅读(六)
[原]AFNetworking源码阅读(六) 本文转载请注明出处 —— polobymulberry-博客园 1. 前言 这一篇的想讲的,一个就是分析一下AFSecurityPolicy文件,看看AF ...
随机推荐
- [iOS]技巧集锦:UICollectionView内容下沉64像素原因和解决方案
现象 UICollectionView的内容在按Home键再回到APP时,会下沉64像素. 原因 页面有NavigationBar,正好是64像素,Controller勾选了Adjust Scroll ...
- 【java开发】数组基本学习
一维数组 定义:具有相同数据类型的一组数据. 声明:int []a=new int[3]; 释义:该数组的数据类型为int型,该数组长度为3,有3个元素 可采用如下方式为元素赋值:a[0]=1; ...
- FineReport中Domino数据库连接方法
1. 概述 Domino是文档型数据库而非关系型数据库,连接Domino可以使用JDBC方式或者ODBC方式,使用JDBC方式需要安装Lotus Domino Driver for JDBC并且此方法 ...
- 理解浮动和position定位
前言 为了更好理解浮动和position,建议先看看我写的这篇文章<Html文档流和文档对象模型DOM理解> 正文 一.浮动 CSS设计float属性的主要目的,是为了实现文本绕排图片的效 ...
- 理解ThreadLocal(之一)
ThreadLocal是什么 在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路.使用这个工具类可以很简洁地编 ...
- POJ3368Frequent values[RMQ 游程编码]
Frequent values Time Limit: 2000MS Memory Limit: 65536K Total Submissions: 17581 Accepted: 6346 ...
- JS中的“!!”
var o={flag:true}; var test=!!o.flag;//等效于var test=o.flag||false; alert(test); 由于对null与undefined用! ...
- jQuery中slice()用法总结
<!DOCTYPE html> <html> <head lang="en"> <meta charset="utf-8&quo ...
- 关于vs2012、tfs2012、windows server 2008r2一些记录
windows server 2008r2安装在虚拟机中,装有tfs2012.sql server 2012. 物理机装有vs2012 1.用vs2012连接tfs时候,会让输入一个有效用户.输入的是 ...
- SQL Server2008从入门到全面精通 SQL数据库视频教程
第1章 SQL Server 2008入门知识:1.SQL SERVER 2008简介2.数据库概念3.关系数据库4.范式5.E-R模型6.SQL Server 2008体系结构7.安装IIS服务8. ...