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 ...
随机推荐
- 标题栏Menu
标题栏menu就是指下图中红框里面的菜单按钮. 标题栏上所有的按钮或者其他元素都定义在xml文件里面,这些文件资源称为menu resource.要在标题栏添加按钮,需要在项目的/res/menu/路 ...
- This text field does not specify an inputType or a hint
android开发过程中突然发现的warning,EditText 报出 “This text field does not specify an inputType or a hint” 原因: ...
- 使用github之前的技能准备
Git的导入 介绍 Git属于分散型版本管理系统,是为版本管理而设计的软件.版本管理就是管理更新的历史记录.它为我们提供了一些在软件开发过程中必不可少的功能,例如记录一款软件添加或更改源代码的过程,回 ...
- [django]django+datatable简单运用于表格中
使用datatable首先需要下载datatable文件,文件主要包括三个文件夹css,img,js相关文件,在django中如何配置呢? 首先需要在模板中引入datatable文件,格式如下: &l ...
- 读 [The Root of Lisp]
首先,在对 Lisp 有一丢丢的了解下读这篇文章会大大激发你学下去的欲望.之后可以看作者的著作<ANSI Common Lisp>. 想要体会一下 Lisp 的强大,本文是不二之选. Co ...
- BZOJ 1061: [Noi2008]志愿者招募 [单纯形法]【学习笔记】
1061: [Noi2008]志愿者招募 Time Limit: 20 Sec Memory Limit: 162 MBSubmit: 3975 Solved: 2421[Submit][Stat ...
- [No00006B]方便的网络下载工具wget 可下载网站目录下的所有文件(可下载整个网站)
wget是linux下命令行的下载工具,功能很强大,它能完成某些下载软件所不能做的,比如如果你想下载一个网页目录下的所有文件,如何做呢?网络用户有时候会遇到需要下载一批文件的情况,有时甚至需要把整个网 ...
- web端功能测试总结(二)
一.输入框测试 在web测试中,我们经常遇到这种输入框的测试,输入框测试看似简单,实际上包含了很多的测试基本的方法,思考逻辑,下面就是我总结出来的一些注意点: 1)验证输入输出信息的一致性 2)输入框 ...
- C#.NET 大型企业信息化系统集成快速开发平台 4.2 版本 - 面向全国标准省市县行政数据基础之上的组织机构管理
由于信息系统庞大.各种业务子系统.各种开发语言开发的业务逻辑.各种年代维护的代码.各种参差不齐的历史遗留信息系统,面向全国的业务系统,面向某个领域的汽运管理信息系统,面向内部的业务系统,面向外部的各种 ...
- 图像旋转 OpenCV实现
经常对一幅图像进行旋转操作,OpenCV中提供了很方便易用的仿射变换函数warpAffine, 通过getRotationMatrix2D可以得到放射变换矩阵(矩阵大小2x3) #include &l ...