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