Backbone源码阅读手记

时间:2023-03-09 03:10:20
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的核心部分,我们可以从中得知:

在Events中维护了一个_events对象,而_events对象中的属性名就代表了一个事件,属性值为一个数组,数组中的元素为包含了注册的回调函数的对象。所以当我们调用on('click', callback, ctx)时,实际上是做了这样的操作:this._events['click'].push({ callback: callback, context: ctx });
Events模块还提供了once、listenTo、stopListening、listenToOnce等有用的方法,但它们都是基于on、off方法实现的,所以这边就不多说了。
另外多说几句:因为javascript的函数也为对象,是一等公民,所以可以方便的通过维护函数对象列表来实现事件机制而不是通过设计模式里的观察者模式来实现,c#中的事件机制也是如此(基于委托)。

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模块

Backbone源码阅读手记

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