Backbone Model 源码简谈 (版本:1.1.0 基础部分完毕)
Model工厂
作为model的主要函数,其实只有12行,特别的简练
1 var Model = Backbone.Model = function(attributes, options) { 2 var attrs = attributes || {}; 3 options || (options = {}); 4 this.cid = _.uniqueId('c'); 5 this.attributes = {}; 6 if (options.collection) this.collection = options.collection; 7 if (options.parse) attrs = this.parse(attrs, options) || {};//parse 889 8 attrs = _.defaults({}, attrs, _.result(this, 'defaults'));//将参数1, _.result(this, 'defaults')对象的属性合并且返回给attr 9 this.set(attrs, options); 10 this.changed = {}; 11 this.initialize.apply(this, arguments); 12 };
通常情况下,我们在使用Model之前,需要调用extend函数对Model进行自定义扩展后,再new 一下,
1 var Man=Backbone.Model.extend({ 2 initialize:function(){ 3 this.bind('change:name',function(){ 4 console.log('change'); 5 }); 6 }, 7 defaults:{name:'jack',sex:'man'}, 8 sayName:function(){ 9 console.log(this.get('name')); 10 }, 11 validate:function(attributes){ 12 if(attributes.name==''){ 13 console.log('fuck'); 14 } 15 } 16 }); 17 var mm=new Man();
这个过程触发了两个十分重要的函数:
1. this.set(attrs, options); 先调用一次set,将我们extend中的defaults对象的键值对儿设置到Model的attributes中去,set方法在Model中是一个重要方法
2. this.initialize.apply(this, arguments); 调用我们extend中定义的 initialize方法
也和backbone的其他模块有了瓜葛:
1.if (options.collection) this.collection = options.collection; 针对collection
调用extend函数扩展了Model
花开两朵各表一支,先来说说Model这个模块下的操作:
从set引出的一串操作:set是一个对象的属性,然后引用了underscore 的 extend方法将这个对象扩展到Model的原型上
set(key,val,options)函数是一个复杂的函数,下面请看源代码:
1 set: function(key, val, options) { 2 var attr, attrs, unset, changes, silent, changing, prev, current; 3 if (key == null) return this; 4 5 // Handle both `"key", value` and `{key: value}` -style arguments. 6 if (typeof key === 'object') { 7 attrs = key; 8 options = val; 9 } else { 10 (attrs = {})[key] = val; 11 } 12 13 options || (options = {}); 14 15 // Run validation. 16 if (!this._validate(attrs, options)) return false; 17 18 // Extract attributes and options. 19 unset = options.unset; 20 silent = options.silent; 21 changes = []; 22 changing = this._changing; 23 this._changing = true; 24 25 if (!changing) { 26 this._previousAttributes = _.clone(this.attributes); 27 this.changed = {}; 28 } 29 current = this.attributes, prev = this._previousAttributes; 30 31 // Check for changes of `id`. 32 if (this.idAttribute in attrs) this.id = attrs[this.idAttribute]; 33 34 // For each `set` attribute, update or delete the current value. 35 for (attr in attrs) { 36 val = attrs[attr]; 37 if (!_.isEqual(current[attr], val)) changes.push(attr); 38 if (!_.isEqual(prev[attr], val)) { 39 this.changed[attr] = val; 40 } else { 41 delete this.changed[attr]; 42 } 43 unset ? delete current[attr] : current[attr] = val; 44 } 45 46 // Trigger all relevant attribute changes. 47 if (!silent) { 48 if (changes.length) this._pending = true; 49 for (var i = 0, l = changes.length; i < l; i++) { 50 this.trigger('change:' + changes[i], this, current[changes[i]], options); 51 } 52 } 53 54 // You might be wondering why there's a `while` loop here. Changes can 55 // be recursively nested within `"change"` events. 56 if (changing) return this; 57 if (!silent) { 58 while (this._pending) { 59 this._pending = false; 60 this.trigger('change', this, options); 61 } 62 } 63 this._pending = false; 64 this._changing = false; 65 return this; 66 }
options参数的一些属性:
unset:如果为true,从attributes中delete掉某个属相
silent: 如果为true,不会触发change事件
validate:如果为true,将允许对set的属性进行验证操作
1.首先进行参数处理前的准备工作:
如果key是字符串一切都好办,把key和val存在attr这个临时对象里。如果key是object,那么val作为options,
2.运行model内置_validate验证函数:该函数功能参看下面的工具函数
3.分拣options中的属性,并且制造changing,其中this._changing属性仅在 set 和 changedAttributes 这两个函数中涉及。
4.开始做真正赋值之前的准备工作:把现有的attributes值存放到 _previousAttributes 中,并生成changed属性,这个操作就注定changed一次性保存属性更改。
5.开始进行循环校验赋值操作,将修改后的值保存到changed属性中。
6.针对options中约束进行处理:
如果options中定义了unset为true,那么从attributes中清除这些属性。
如果定义了silent为true,那么将触发在model-class绑定的 change事件,注意只能是 change事件。接下来执行没有确定属性名称的单纯 change 事件,这就意味着每又一次属性改变就要执行依次 纯change 事件
7.返回当前引用对象 也就是 model-object
set分析完毕。
从set函数分析我们得到的结论:
1.set所操作的attributes是model-object的attributes,而不是model-class原型上的attributes,这就导致我们在声明model-class extend的attributes毫无用处
2. unset 标注所执行的操作并不会计入到 changed属性中
3. extend方法参数中的default属性,在工厂函数执行的时候就用set方法放到model-object 的attributes中去了。
再说extend函数:
这个extend函数并没有存在于Model的原型中,而是Model一个私有方法,具体实现方法如下:
1 var extend = function(protoProps, staticProps) { 2 var parent = this; 3 var child; 4 5 // The constructor function for the new subclass is either defined by you 6 // (the "constructor" property in your `extend` definition), or defaulted 7 // by us to simply call the parent's constructor. 8 if (protoProps && _.has(protoProps, 'constructor')) {//检查protoProps是否为原型 9 child = protoProps.constructor; 10 } else { 11 child = function(){ return parent.apply(this, arguments); }; 12 } 13 14 // Add static properties to the constructor function, if supplied. 15 _.extend(child, parent, staticProps); 16 17 // Set the prototype chain to inherit from `parent`, without calling 18 // `parent`'s constructor function. 19 var Surrogate = function(){ this.constructor = child; }; 20 Surrogate.prototype = parent.prototype; 21 child.prototype = new Surrogate; 22 23 // Add prototype properties (instance properties) to the subclass, 24 // if supplied. 25 if (protoProps) _.extend(child.prototype, protoProps); 26 27 // Set a convenience property in case the parent's prototype is needed 28 // later. 29 child.__super__ = parent.prototype; 30 31 return child; 32 }; 33 34 // Set up inheritance for the model, collection, router, view and history. 35 Model.extend = Collection.extend = Router.extend = View.extend = History.extend = extend;
这个extend实在有趣,因为它翻来覆去的操作原型链达到扩展对象具有私有原型的目的。看看我们extend一个Model需要进行哪些步骤:
1.先设立一个child函数,这个child函数返回了 Model自执行时的结果,很显然从前面我们知道这根本没有什么返回值,主要就是为了执行以下Model。
2.使用underscore的extend扩展child,当然这个扩展无非就是把Model的属性和extend第二个参数的属性加到child上。
3.建立一个surrogate函数,function(){ this.constructor = child; };
4.将surrogate函数的原型赋值为Model的原型,就是包含set的那一串
5.然后让child的原型变为 new surrogate;
6.然后再用underscore的extend将我们传个Model.extend函数的第一个参数扩展给child 这是关键的一步
7.返回child,让child成为我们要使用的那个我们自定义的model-class
这么做是为什么呢?它形成了由 model-object -> model-class原型 -> Model原型 的一条原型链,有了明确的继承与被继承的关系。
Model的验证
从上面的代码我们可以看到,我们在Model的扩展Man中写了验证函数,但是这个验证什么时候被触发呢?那就是在我们调用model-object的save方法的时候。
save(key,val,options)
save一共做了两档子事,上面是一件,还有一件是做一次ajax提交,还是通过trigger函数触发异步事件。
下面是save的源码:
1 // Set a hash of model attributes, and sync the model to the server. 2 // If the server returns an attributes hash that differs, the model's 3 // state will be `set` again. 4 save: function(key, val, options) { 5 var attrs, method, xhr, attributes = this.attributes; 6 7 // Handle both `"key", value` and `{key: value}` -style arguments. 8 if (key == null || typeof key === 'object') { 9 attrs = key; 10 options = val; 11 } else { 12 (attrs = {})[key] = val; 13 } 14 15 options = _.extend({validate: true}, options); 16 17 // If we're not waiting and attributes exist, save acts as 18 // `set(attr).save(null, opts)` with validation. Otherwise, check if 19 // the model will be valid when the attributes, if any, are set. 20 if (attrs && !options.wait) { 21 if (!this.set(attrs, options)) return false; 22 } else { 23 if (!this._validate(attrs, options)) return false; 24 } 25 26 // Set temporary attributes if `{wait: true}`. 27 if (attrs && options.wait) { 28 this.attributes = _.extend({}, attributes, attrs); 29 } 30 31 // After a successful server-side save, the client is (optionally) 32 // updated with the server-side state. 33 if (options.parse === void 0) options.parse = true; 34 var model = this; 35 var success = options.success; 36 options.success = function(resp) { 37 // Ensure attributes are restored during synchronous saves. 38 model.attributes = attributes; 39 var serverAttrs = model.parse(resp, options); 40 if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); 41 if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { 42 return false; 43 } 44 if (success) success(model, resp, options); 45 model.trigger('sync', model, resp, options); 46 }; 47 wrapError(this, options); 48 49 method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); 50 if (method === 'patch') options.attrs = attrs; 51 xhr = this.sync(method, this, options); 52 53 // Restore attributes. 54 if (attrs && options.wait) this.attributes = attributes; 55 56 return xhr; 57 },
options的属性:
validate: bool 是否对值进行验证
wait: bool 是否等待异步提交后保存(前提 key不能为空)
parse: bool 默认 true 不明
success: save成功后回调
其余参数:参见 Backbone 上的方法
save的具体执行步骤是这样的:
1.前期对参数处理,同set方法
2.为options附加默认validate 为 true属性,默认对值进行验证
3.重写options的success函数,将原来的success在重写的里面执行
重写增加以下几点内容:整理ajax返回值并将其set到model-object的attributes中,然后执行一次自定义的success,最后再进行一次ajax造成递归ajax请求的递归回调。
4.收集错误并抛出错误:
1 // Wrap an optional error callback with a fallback error event. 2 var wrapError = function(model, options) { 3 var error = options.error; 4 options.error = function(resp) { 5 if (error) error(model, resp, options); 6 model.trigger('error', model, resp, options); 7 }; 8 };
5.判定使用何种协议
6.手动执行sync ajax请求函数
7.返回xhr对象
save源码分析结束
Model获取服务端数据
上面我们提到的save方法涉及了ajax向服务端提交数据做保存,所以跟着ajax获取这条线,我们看看fetch是怎么从服务端获取数据的。
fetch(options)
参数options是一个对象,里面包含了我们fetch函数需要操作的一些配置项,包括:
常用的有:url success error
1 // Fetch the default set of models for this collection, resetting the 2 // collection when they arrive. If `reset: true` is passed, the response 3 // data will be passed through the `reset` method instead of `set`. 4 fetch: function(options) { 5 options = options ? _.clone(options) : {}; 6 if (options.parse === void 0) options.parse = true; 7 var success = options.success; 8 var collection = this; 9 options.success = function(resp) { 10 var method = options.reset ? 'reset' : 'set'; 11 collection[method](resp, options); 12 if (success) success(collection, resp, options); 13 collection.trigger('sync', collection, resp, options); 14 }; 15 wrapError(this, options); 16 return this.sync('read', this, options); 17 },
通过查看源码,我们发现,fetch进行了以下三步重要的工作:
1.根据options的参数决定是否调用set 还是 reset
2.执行success方法
3.通过trigger触发sync 同步事件
4.返回 this.sync('read', this, options) 这是一个对象,这个对象包含各种类似xhr对象的操作,用来干预整个ajax的活动进程。
以上就是model-object操作一个对象一些简要的流程,当然model-object有很多的方法供大家使用:
操作属性方法:
set上面已经提到,跟它相对应的就是unset 和clear 清理对象属性方法
unset(attr,options)
源码如下:
1 // Remove an attribute from the model, firing `"change"`. `unset` is a noop 2 // if the attribute doesn't exist. 3 unset: function(attr, options) { 4 return this.set(attr, void 0, _.extend({}, options, {unset: true})); 5 },
是不是很简单,直接就套用了 set方法嘛。unset会触发相应的事件change事件
clear(options)
源码如下:
1 // Clear all attributes on the model, firing `"change"`. 2 clear: function(options) { 3 var attrs = {}; 4 for (var key in this.attributes) attrs[key] = void 0; 5 return this.set(attrs, _.extend({}, options, {unset: true})); 6 },
也是套用了set方法,同样触发了所有和属性相关的事件。
有set必有get,同样实现方式超级简单:
get(attr)
以下是源码:
1 // Get the value of an attribute. 2 get: function(attr) { 3 return this.attributes[attr]; 4 },
简单的大快人心
has(attr)
检测是否存在该属性的实现也是操作attributes属性,套用了get
1 // Returns `true` if the attribute contains a value that is not null 2 // or undefined. 3 has: function(attr) { 4 return this.get(attr) != null; 5 },
关于model的总结:
1.工厂函数传入attributes参数时,并不会触发change事件,只有model-object显示调用 set save 的时候才会触发。
因为model-class虽然在参数初始化attributes的时候虽然调用了set,但是this上并没有_events属性,所以没法触发绑定的方法。
2.set时至少触发2次all事件,save在url有配置的情况下则会至少触发3次all事件:sync 的request一次,回调success中的递归sync 一次,最后sync 中的request一次。
因为 all 事件只要执行 trigger函数 就会触发。
-----------------------------------------Model基础到此结束,下面是Model中的工具函数--------------------------
1._validate(attrs, options)
参数:attrs object 表示需要设置为属性的键值对
options object 该键值对设置时验证方法
作用: 使用model-class 中自定义的validate方法对attrs进行验证处理
原理:首先检查options中及this(model-object)中是否存在validate方法,如果没有返回true,表验证通过
其次将attrs和model-object的 attributes合并,作为被验证的对象
调用this.validate方法进行验证并把返回值放到 error 和 this.validationError中
最后触发 invalid 事件
源码:
1 // Run validation against the next complete set of model attributes, 2 // returning `true` if all is well. Otherwise, fire an `"invalid"` event. 3 _validate: function(attrs, options) { 4 if (!options.validate || !this.validate) return true; 5 attrs = _.extend({}, this.attributes, attrs); 6 var error = this.validationError = this.validate(attrs, options) || null; 7 if (!error) return true; 8 this.trigger('invalid', this, error, _.extend(options, {validationError: error})); 9 return false; 10 }
2.escape(attr)
参数:属性名 string
作用:将该属性进行html转移
原理:调用underscore的escape方法实现。
主要就是讲该对象下的特殊字符进行html转移替换而已:
1 var escapeMap = { 2 '&': '&', 3 '<': '<', 4 '>': '>', 5 '"': '"', 6 "'": ''', 7 '`': '`' 8 };
源码:
1 // Get the HTML-escaped value of an attribute. 2 escape: function(attr) { 3 return _.escape(this.get(attr)); 4 },