Backbone Collection 源码简谈
一切由一个例子引发:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var Man=Backbone.Model.extend({ 2 initilize:function(){ 3 this.bind('change:name',function(){ 4 console.log('改名了'); 5 }); 6 }, 7 validate:function(){ 8 if(this.attributes.name=='jack'){ 9 console.log('不同意'); 10 } 11 } 12 }); 13 var man=new Man(); 14 man.set({'name':'jack','sex':'man','age':'12'},{validate:false}); 15 16 var Mans=Backbone.Collection.extend({ 17 model:man 18 }); 19 var tom=new Man({name:'tom'}); 20 var mary=new Man({name:'mary'}); 21 var mans=new Mans(); 22 mans.add(tom); 23 mans.add(mary); 24 mans.each(function(m){ 25 console.log(m.get('name'));// tom mary 26 });
Collection的实例化声明很简单,初级使用只用了model属性,然后涉及到了 add方法和each遍历方法。
Collection 工厂
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Create a new **Collection**, perhaps to contain a specific type of `model`. 2 // If a `comparator` is specified, the Collection will maintain 3 // its models in sort order, as they're added and removed. 4 var Collection = Backbone.Collection = function(models, options) { 5 options || (options = {}); 6 if (options.model) this.model = options.model; 7 if (options.comparator !== void 0) this.comparator = options.comparator; 8 this._reset(); 9 this.initialize.apply(this, arguments); 10 if (models) this.reset(models, _.extend({silent: true}, options)); 11 };
以上是collection的源码
collection的参数:
models: array 由model-object组成的数组。
options: object 配置型信息,包括以下几个属性:
model: function
comparator: string
silent: bool 是否禁止触发事件
sort: bool 是否进行排序
at:number 用于确定添加models时从何位置添加
add:
merge:
remove:
工厂函数执行步骤如下:
1.进行参数的调整和初始化分配
2.对collection-object进行重置,重置操作代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Private method to reset all internal state. Called when the collection 2 // is first initialized or reset. 3 _reset: function() { 4 this.length = 0; 5 this.models = []; 6 this._byId = {}; 7 },
3.执行collection-object的初始化操作,initialize函数默认为空函数
4.关键的最后一步,使用reset方法对collection-object附加属性
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // When you have more items than you want to add or remove individually, 2 // you can reset the entire set with a new list of models, without firing 3 // any granular `add` or `remove` events. Fires `reset` when finished. 4 // Useful for bulk operations and optimizations. 5 reset: function(models, options) { 6 options || (options = {}); 7 for (var i = 0, l = this.models.length; i < l; i++) { 8 this._removeReference(this.models[i]); 9 } 10 options.previousModels = this.models; 11 this._reset(); 12 models = this.add(models, _.extend({silent: true}, options)); 13 if (!options.silent) this.trigger('reset', this, options); 14 return models; 15 },
工厂函数分解完毕。下面我们就来看看关键的 reset方法都做了什么?
reset(models,options)
reset函数的两个参数均来自工厂函数
reset执行步骤如下:
1.首先还是参数的准备
2.遍历models并对models进行_removeReference处理
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Internal method to sever a model's ties to a collection. 2 _removeReference: function(model) { 3 if (this === model.collection) delete model.collection; 4 model.off('all', this._onModelEvent, this); 5 },
通过查看源码,我们发现,处理后的model-object没有了collection属性,因为该属性保存了它原来隶属于的collection。 同时暂时关闭了model-object上的all方法。
3.再次调用_reset()对collection对象进行重置
4.调用add方法,其实add就是set方法,下面我们讲到add的时候就直接说set方法:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Add a model, or list of models to the set. 2 add: function(models, options) { 3 return this.set(models, _.extend({merge: false}, options, addOptions)); 4 },
5.执行 reset 事件,如有则执行。
6.返回参数1
reset分解结束。
上一单元我们提到了reset中调用了add,而add的实质其实就是set方法,那么接下来就八一八set
set(models, options)
其参数来自于工厂函数。不同的是,add对options中的merge字段做了强制处理。导致set接收的options变了味儿,其中就附加了
var addOptions = {add: true, remove: false};
和{merge:false}
以下是set源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Update a collection by `set`-ing a new list of models, adding new ones, 2 // removing models that are no longer present, and merging models that 3 // already exist in the collection, as necessary. Similar to **Model#set**, 4 // the core operation for updating the data contained by the collection. 5 set: function(models, options) { 6 options = _.defaults({}, options, setOptions); 7 if (options.parse) models = this.parse(models, options); 8 var singular = !_.isArray(models); 9 models = singular ? (models ? [models] : []) : _.clone(models); 10 var i, l, id, model, attrs, existing, sort; 11 var at = options.at; 12 var targetModel = this.model; 13 var sortable = this.comparator && (at == null) && options.sort !== false; 14 var sortAttr = _.isString(this.comparator) ? this.comparator : null; 15 var toAdd = [], toRemove = [], modelMap = {}; 16 var add = options.add, merge = options.merge, remove = options.remove; 17 var order = !sortable && add && remove ? [] : false; 18 19 // Turn bare objects into model references, and prevent invalid models 20 // from being added. 21 for (i = 0, l = models.length; i < l; i++) { 22 attrs = models[i]; 23 if (attrs instanceof Model) { 24 id = model = attrs; 25 } else { 26 id = attrs[targetModel.prototype.idAttribute]; 27 } 28 29 // If a duplicate is found, prevent it from being added and 30 // optionally merge it into the existing model. 31 if (existing = this.get(id)) { 32 if (remove) modelMap[existing.cid] = true; 33 if (merge) { 34 attrs = attrs === model ? model.attributes : attrs; 35 if (options.parse) attrs = existing.parse(attrs, options); 36 existing.set(attrs, options); 37 if (sortable && !sort && existing.hasChanged(sortAttr)) sort = true; 38 } 39 models[i] = existing; 40 41 // If this is a new, valid model, push it to the `toAdd` list. 42 } else if (add) { 43 model = models[i] = this._prepareModel(attrs, options); 44 if (!model) continue; 45 toAdd.push(model); 46 47 // Listen to added models' events, and index models for lookup by 48 // `id` and by `cid`. 49 model.on('all', this._onModelEvent, this); 50 this._byId[model.cid] = model; 51 if (model.id != null) this._byId[model.id] = model; 52 } 53 if (order) order.push(existing || model); 54 } 55 56 // Remove nonexistent models if appropriate. 57 if (remove) { 58 for (i = 0, l = this.length; i < l; ++i) { 59 if (!modelMap[(model = this.models[i]).cid]) toRemove.push(model); 60 } 61 if (toRemove.length) this.remove(toRemove, options); 62 } 63 64 // See if sorting is needed, update `length` and splice in new models. 65 if (toAdd.length || (order && order.length)) { 66 if (sortable) sort = true; 67 this.length += toAdd.length; 68 if (at != null) { 69 for (i = 0, l = toAdd.length; i < l; i++) { 70 this.models.splice(at + i, 0, toAdd[i]); 71 } 72 } else { 73 if (order) this.models.length = 0; 74 var orderedModels = order || toAdd; 75 for (i = 0, l = orderedModels.length; i < l; i++) { 76 this.models.push(orderedModels[i]); 77 } 78 } 79 } 80 81 // Silently sort the collection if appropriate. 82 if (sort) this.sort({silent: true}); 83 84 // Unless silenced, it's time to fire all appropriate add/sort events. 85 if (!options.silent) { 86 for (i = 0, l = toAdd.length; i < l; i++) { 87 (model = toAdd[i]).trigger('add', model, this, options); 88 } 89 if (sort || (order && order.length)) this.trigger('sort', this, options); 90 } 91 92 // Return the added (or merged) model (or models). 93 return singular ? models[0] : models; 94 },
1.老一套,参数准备,把models强制变成数组
2.局部变量的准备,为运行打好基础。
3.遍历models这个数组,根据不同情况取id,第一种id其实就是model-object.
4.在遍历的过程中判断这个model-object是否已经存在于collection-object中(即检测_byId对象,get也是用于检测_byId),如果存在:
options.remove是否为true,如果是则整理modelMap
options.merge 如果为true, 将已有的model-object和当前的model-object合并
将处理完的 existing model-object放入models数组的当前位置中
5.如果不存在且options.add为true:
将当前model存入toAdd数组
开启该model-object的all事件(在工厂函数的时候关过一次)
将该model-object存入_byId对象
6.如果order存在且是数组,则将existing||model push进去 循环到此结束
7.如果options.remove存在
遍历循环this,也就是collection-class={length:model-object个数,_byId:{cid:model-object},models:[model-object]}
还记得上面那个循环维护的modelMap吗?这时就要派上用场,modelMap里面记录的都是已经存在的model-object.cid:true,根据上述判断,对于不满足条件的model-object将放入toRemove数组
遍历完毕后检查toRemove数组,然后执行remove操作,remove操作详细看分割线下之下第一个函数
8.然后检测是否存在toAdd或者order数组:
如果不需要排序且指定了at 位置,则按照位置插入model-object
否则将this.models置空,将order或toAdd存入其中
9.如果存在排序,则调用this.sort,sort方法看分割线下第二个函数
10.进行事件处理,分别根据情况选择触发add 和 sort函数
11.根据传入参数是否单一决定返回model-object 或 models
至此set函数分析完毕
结论:建立一个collection-object,依次会调用: 工厂函数->reset函数->set函数
工厂函数的一条线分析完毕,综上分析collection的核心其实就是models数组和_byId对象
collection的相关操作
at(index):获取该索引下的model
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Get the model at the given index. 2 at: function(index) { 3 return this.models[index]; 4 },
其实就是取出collection-object.models[index]
-----------------------------------------------collection 内部函数----------------------------------
1.remove(models, options)
参数: models:model-object 的集合
options: object 配置对象
二者均来自collection 工厂函数
作用: 从collection中删除单个model-object 或 多个model-object
原理: 1.还是进行参数的保障性处理和局部变量的准备
2.遍历这个即将要被删除掉的数组
3.在遍历过程中针对_byId进行删除工作
4.在遍历过程中针对collection-object.models属性进行删除操作
5.对collection-object.length属性进行操作
6.在遍历过程中options.silent没有声明,则触发remove事件,如果有all也会触发
7.关闭当前models[i]的all事件,遍历结束
8.返回经过处理后的第一个参数
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Remove a model, or a list of models from the set. 2 remove: function(models, options) { 3 var singular = !_.isArray(models); 4 models = singular ? [models] : _.clone(models); 5 options || (options = {}); 6 var i, l, index, model; 7 for (i = 0, l = models.length; i < l; i++) { 8 model = models[i] = this.get(models[i]); 9 if (!model) continue; 10 delete this._byId[model.id]; 11 delete this._byId[model.cid]; 12 index = this.indexOf(model); 13 this.models.splice(index, 1); 14 this.length--; 15 if (!options.silent) { 16 options.index = index; 17 model.trigger('remove', model, this, options); 18 } 19 this._removeReference(model); 20 } 21 return singular ? models[0] : models; 22 },
2.sort(options)
参数:options object 配置对象
作用:用于collection中model-object的排序,一般我们不会用到它,使用sort的前提是collection-object中定义comparator属性
原理:1.首先检测是否包含comparator属性,没有则抛出 Cannot sort a set without a comparator 错误
2.然后检测 comparator是否为字符串或长度是否为1
通过检测则调用sortBy函数进行排序
否则使用underscore.bind方法排序
3.根据options.silent决定是否触发sort事件
4.返回this collection-object
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Force the collection to re-sort itself. You don't need to call this under 2 // normal circumstances, as the set will maintain sort order as each item 3 // is added. 4 sort: function(options) { 5 if (!this.comparator) throw new Error('Cannot sort a set without a comparator'); 6 options || (options = {}); 7 8 // Run sort based on type of `comparator`. 9 if (_.isString(this.comparator) || this.comparator.length === 1) { 10 this.models = this.sortBy(this.comparator, this); 11 } else { 12 this.models.sort(_.bind(this.comparator, this)); 13 } 14 15 if (!options.silent) this.trigger('sort', this, options); 16 return this; 17 },
看到这里大家有些奇怪。这个sortBy和_.bind究竟是啥东西呢?咱们接着往下扒:
sortBy集成于这种机制: 它的兄弟还有 countBy 和 groupBy
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Underscore methods that take a property name as an argument. 2 var attributeMethods = ['groupBy', 'countBy', 'sortBy']; 3 4 // Use attributes instead of properties. 5 _.each(attributeMethods, function(method) { 6 Collection.prototype[method] = function(value, context) { 7 var iterator = _.isFunction(value) ? value : function(model) { 8 return model.get(value); 9 }; 10 return _[method](this.models, iterator, context); 11 }; 12 });
这下我们就知道 this.comparator其实是一个字符串,然后这个sortBy方法走的是underscore的sortBy方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Sort the object's values by a criterion produced by an iteratee. 2 _.sortBy = function(obj, iteratee, context) { 3 iteratee = _.iteratee(iteratee, context); 4 return _.pluck(_.map(obj, function(value, index, list) { 5 return { 6 value: value, 7 index: index, 8 criteria: iteratee(value, index, list) 9 }; 10 }).sort(function(left, right) { 11 var a = left.criteria; 12 var b = right.criteria; 13 if (a !== b) { 14 if (a > b || a === void 0) return 1; 15 if (a < b || b === void 0) return -1; 16 } 17 return left.index - right.index; 18 }), 'value'); 19 };
而underscore的sortBy利用了原生js数组的sort方法,通过前后两个对象抽出相同属性比较可以为对象数组排序的特性进行排序,只不过实现的方式有些绕弯,有兴趣的同学可以自己探索一下。原理就是:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 var arr=[{name:'jack',age:6},{name:'jim',age:4},{name:'tom',age:7}]; 2 var o=arr.sort(function(left,right){ 3 var a=left.age; 4 var b=right.age; 5 return a-b; 6 }); 7 console.log(o); 8 // {name:'jim',age:4} ,{name:'jack',age:6},{name:'tom',age:7}
3.at(index)
参数: number 索引值
作用: 获取该索引下的model-object
原理: 操作collection-object数组
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Get the model at the given index. 2 at: function(index) { 3 return this.models[index]; 4 },
4.继承underscore的函数:
继承的时候,backbone做了一些小小的修改,当执行方法时,underscore中方法所操作的第一个参数obj就是collection-object.models ,第二个参数是我们在backbone调用时传的参数
// 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']; // Mix in each Underscore method as a proxy to `Collection#models`. _.each(methods, function(method) { Collection.prototype[method] = function() { var args = slice.call(arguments); args.unshift(this.models); return _[method].apply(_, args); }; });
5.clone()
作用:将一个collection-object进行深度克隆
原理:new 一个collection-object出来,需要传入当前this对象的models
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Create a new collection with an identical list of models as this one. 2 clone: function() { 3 return new this.constructor(this.models); 4 },
6.get(obj)
参数: string/object 可以是字符串或者{id:''} {cid:''} 组成的对象
作用: 通过id获取model-object
原理:通过匹配_byId对象,查找相应的模型对象
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Get a model from the set by id. 2 get: function(obj) { 3 if (obj == null) return void 0; 4 return this._byId[obj.id] || this._byId[obj.cid] || this._byId[obj]; 5 },
7.where(attrs,first)
参数:attrs object 需要查找的 key-value组合,first bool 是否得到第一个满足条件的该对象
作用: 根据key-value组合查找到相对应的model-object
原理:如果first存在,则使用继承自underscore的 find方法遍历寻找目标对象,否则调用filter遍历寻找该目标
源码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
1 // Return models with matching attributes. Useful for simple cases of 2 // `filter`. 3 where: function(attrs, first) { 4 if (_.isEmpty(attrs)) return first ? void 0 : []; 5 return this[first ? 'find' : 'filter'](function(model) { 6 for (var key in attrs) { 7 if (attrs[key] !== model.get(key)) return false; 8 } 9 return true; 10 }); 11 },