backbone.Model
backbone的model(模型),用来存储数据,交互数据,数据验证,在view里面可以直接监听model来达到model一改变,就通知视图.
这个里面的代码是从backbone里面剥离出来,然后一点一点研究和调试出来的,可以单独运行,依赖underscore,jquery或者是zepto event.js是剥离出来的Backbone.Events
<!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <title>backbone</title> <style type="text/css"> *{padding:0;margin:0;} .wrap{width:960px; margin: 100px auto; padding: 20px 0;} ul{ list-style: none;} </style> </head> <body> <div class="wrap"> <div id="a1"></div> <div id="a2"></div> <div id="a3"></div> </div> <script src="https://files.cnblogs.com/wtcsy/jquery.js"></script> <script src="https://files.cnblogs.com/wtcsy/underscore.js"></script> <script src="https://files.cnblogs.com/wtcsy/events.js"></script> <script> (function(){ // Backbone.Model // -------------- // Backbone **Models** are the basic data object in the framework -- // frequently representing a row in a table in a database on your server. // A discrete chunk of data and a bunch of useful, related methods for // performing computations and transformations on that data. // Create a new model with the specified attributes. A client id (`cid`) // is automatically generated and assigned for you. var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); //每个molde都有一个cid 唯一的标识 this.cid = _.uniqueId('c'); //这个是存放设置值得hash列表 this.attributes = {}; //看这个model是属于哪个collection if (options.collection) this.collection = options.collection; //格式化参数 默认是不做变化的,可以自己扩展parse方法实现 if (options.parse) attrs = this.parse(attrs, options) || {}; attrs = _.defaults({}, attrs, _.result(this, 'defaults')); this.set(attrs, options); // 被改变了的值 this.changed = {}; this.initialize.apply(this, arguments); }; _.extend(Model.prototype, Backbone.Events, { // A hash of attributes whose current and previous value differ. //存放 与之前attributes里面改变了的值 changed: null, //验证失败后返回的信息 // The value returned during the last failed validation. validationError: null, // The default name for the JSON `id` attribute is `"id"`. MongoDB and // CouchDB users may want to set this to `"_id"`. idAttribute: 'id', // The function that will generate an id for a model given that model's // attributes. generateId: function (attrs) { return attrs[this.idAttribute]; }, // Initialize is an empty function by default. Override it with your own // initialization logic. // 实例化一个model的时候总会被调用的方法 initialize: function(){}, // Return a copy of the model's `attributes` object. // 复制model.的attributes的属性 toJSON: function(options) { return _.clone(this.attributes); }, // Proxy `Backbone.sync` by default -- but override this if you need // custom syncing semantics for *this* particular model. sync: function() { return Backbone.sync.apply(this, arguments); }, // Get the value of an attribute. get: function(attr) { return this.attributes[attr]; }, // Get the HTML-escaped value of an attribute. escape: function(attr) { return _.escape(this.get(attr)); }, // Remove an attribute from the model, firing `"change"`. `unset` is a noop // if the attribute doesn't exist. // 删除model上的数据 触发监听 change 和 unset的回调 unset: function(attr, options) { return this.set(attr, void 0, _.extend({}, options, {unset: true})); }, // Clear all attributes on the model, firing `"change"`. clear: function(options) { var attrs = {}; for (var key in this.attributes) attrs[key] = void 0; return this.set(attrs, _.extend({}, options, {unset: true})); }, // Determine if the model has changed since the last `"change"` event. // If you specify an attribute name, determine if that attribute has changed. // 查看某个是属性值 是否被修改了 hasChanged: function(attr) { if (attr == null) return !_.isEmpty(this.changed); return _.has(this.changed, attr); }, // Return an object containing all the attributes that have changed, or // false if there are no changed attributes. Useful for determining what // parts of a view need to be updated and/or what attributes need to be // persisted to the server. Unset attributes will be set to undefined. // You can also pass an attributes object to diff against the model, // determining if there *would be* a change. changedAttributes: function(diff) { if (!diff) return this.hasChanged() ? _.clone(this.changed) : false; var val, changed = false; var old = this._changing ? this._previousAttributes : this.attributes; for (var attr in diff) { if (_.isEqual(old[attr], (val = diff[attr]))) continue; (changed || (changed = {}))[attr] = val; } return changed; }, // Get the previous value of an attribute, recorded at the time the last // `"change"` event was fired. //取改变了attribute之前的某个属性值 previous: function(attr) { if (attr == null || !this._previousAttributes) return null; return this._previousAttributes[attr]; }, // Get all of the attributes of the model at the time of the previous // `"change"` event. //获取改变了attribute之前 previousAttributes: function() { return _.clone(this._previousAttributes); }, // Set a hash of model attributes on the object, firing `"change"`. This is // the core primitive operation of a model, updating the data and notifying // anyone who needs to know about the change in state. The heart of the beast. set: function(key, val, options) { var attr, attrs, unset, changes, silent, changing, prev, current; if (key == null) return this; // Handle both `"key", value` and `{key: value}` -style arguments. // 根据传参的不同 统一成key : value的形式 if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options || (options = {}); // Run validation. // 如果需要验证数据格式, 进行验证, 验证不通过 则返回 if (!this._validate(attrs, options)) return false; // Extract attributes and options. // unset表示删除 // changes 是存放改变值得数组 // changing 属性值是否正在改变中 unset = options.unset; silent = options.silent; changes = []; changing = this._changing; this._changing = true; //如果不是在改变值得进行中 复制this.attributes 到 this._previousAttributes if (!changing) { this._previousAttributes = _.clone(this.attributes); this.changed = {}; } current = this.attributes, prev = this._previousAttributes; // For each `set` attribute, update or delete the current value. for (attr in attrs) { val = attrs[attr]; // 如果设置的属性的值,和当前的值不一样 放到changes里面去 if (!_.isEqual(current[attr], val)) changes.push(attr); // 如果设置的值和之前的值 一样 this.changed删除掉该属性 不一样 添加到this.changed里面去 if (!_.isEqual(prev[attr], val)) { this.changed[attr] = val; } else { delete this.changed[attr]; } unset ? delete current[attr] : current[attr] = val; } var prevId = this.id; this.id = this.generateId(current); if (prevId !== this.id) this.trigger('change-id', this, prevId, options); // Trigger all relevant attribute changes. // 触发改变了属性值的相关的回调事件 if (!silent) { if (changes.length) this._pending = options; for (var i = 0, length = changes.length; i < length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } // You might be wondering why there's a `while` loop here. Changes can // be recursively nested within `"change"` events. // if (changing) return this; if (!silent) { while (this._pending) { options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; this._changing = false; return this; }, // **parse** converts a response into the hash of attributes to be `set` on // the model. The default implementation is just to pass the response along. parse: function(resp, options) { return resp; }, // Check if the model is currently in a valid state. isValid: function(options) { return this._validate({}, _.extend(options || {}, { validate: true })); }, // Run validation against the next complete set of model attributes, // returning `true` if all is well. Otherwise, fire an `"invalid"` event. _validate: function(attrs, options) { //在实例化的时候需要传入验证函数validate //然后每次设置值的时候都进行验证 //验证失败 触发invalid的回调事件 if (!options.validate || !this.validate) return true; attrs = _.extend({}, this.attributes, attrs); var error = this.validationError = this.validate(attrs, options) || null; if (!error) return true; this.trigger('invalid', this, error, _.extend(options, {validationError: error})); return false; }, // Fetch the default set of models for this collection, resetting the // collection when they arrive. If `reset: true` is passed, the response // data will be passed through the `reset` method instead of `set`. fetch: function(options) { options = options ? _.clone(options) : {}; if (options.parse === void 0) options.parse = true; var success = options.success; var collection = this; options.success = function(resp) { var method = options.reset ? 'reset' : 'set'; collection[method](resp, options); if (success) success(collection, resp, options); collection.trigger('sync', collection, resp, options); }; wrapError(this, options); return this.sync('read', this, options); }, // Set a hash of model attributes, and sync the model to the server. // If the server returns an attributes hash that differs, the model's // state will be `set` again. save: function(key, val, options) { var attrs, method, xhr, attributes = this.attributes; // Handle both `"key", value` and `{key: value}` -style arguments. if (key == null || typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options = _.extend({validate: true}, options); // If we're not waiting and attributes exist, save acts as // `set(attr).save(null, opts)` with validation. Otherwise, check if // the model will be valid when the attributes, if any, are set. if (attrs && !options.wait) { if (!this.set(attrs, options)) return false; } else { if (!this._validate(attrs, options)) return false; } // Set temporary attributes if `{wait: true}`. if (attrs && options.wait) { this.attributes = _.extend({}, attributes, attrs); } // After a successful server-side save, the client is (optionally) // updated with the server-side state. if (options.parse === void 0) options.parse = true; var model = this; var success = options.success; options.success = function(resp) { // Ensure attributes are restored during synchronous saves. model.attributes = attributes; var serverAttrs = model.parse(resp, options); if (options.wait) serverAttrs = _.extend(attrs || {}, serverAttrs); if (_.isObject(serverAttrs) && !model.set(serverAttrs, options)) { return false; } if (success) success(model, resp, options); model.trigger('sync', model, resp, options); }; wrapError(this, options); method = this.isNew() ? 'create' : (options.patch ? 'patch' : 'update'); if (method === 'patch') options.attrs = attrs; xhr = this.sync(method, this, options); // Restore attributes. if (attrs && options.wait) this.attributes = attributes; return xhr; }, // A model is new if it has never been saved to the server, and lacks an id. isNew: function() { return this.id == null; } }); // Helper function to correctly set up the prototype chain, for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. //第一个参数是要扩展到原型上的对象, 第2个参数是静态方法扩展到构造函数上去的 var extend = function(protoProps, staticProps) { var parent = this; var child; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent's constructor. if (protoProps && _.has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function(){ return parent.apply(this, arguments); }; } // Add static properties to the constructor function, if supplied. //将静态方法和 parent上的静态方法一起扩展到child上面去 _.extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. //创建一个新的构造含糊Surrogate ; //this.constructor = child的意思是 Surrogate实例化后的对象 让对象的构造函数指向child // Surrogate的原型就是parent的原型 // 然后实例化给child的原型, // 这里不是直接从new parent给child.prototype 而是创建一个新的构造函数,我也不知道为啥要这样 var Surrogate = function(){ this.constructor = child; }; Surrogate.prototype = parent.prototype; child.prototype = new Surrogate; // Add prototype properties (instance properties) to the subclass, // if supplied. // 把第一个参数上的属性扩展到child.prototype if (protoProps) _.extend(child.prototype, protoProps); // Set a convenience property in case the parent's prototype is needed // later. // 拿一个属性引用父的原型, 以免以后要用到. child.__super__ = parent.prototype; return child; }; Model.extend = extend; // Backbone.sync // ------------- // Override this function to change the manner in which Backbone persists // models to the server. You will be passed the type of request, and the // model in question. By default, makes a RESTful Ajax request // to the model's `url()`. Some possible customizations could be: // // * Use `setTimeout` to batch rapid-fire updates into a single request. // * Send up the models as XML instead of JSON. // * Persist models via WebSockets instead of Ajax. // // Turn on `Backbone.emulateHTTP` in order to send `PUT` and `DELETE` requests // as `POST`, with a `_method` parameter containing the true HTTP method, // as well as all requests with the body as `application/x-www-form-urlencoded` // instead of `application/json` with the model in a param named `model`. // Useful when interfacing with server-side languages like **PHP** that make // it difficult to read the body of `PUT` requests. // Map from CRUD to HTTP for our default `Backbone.sync` implementation. var methodMap = { 'create': 'POST', 'update': 'PUT', 'patch': 'PATCH', 'delete': 'DELETE', 'read': 'GET' }; // Turn on `emulateHTTP` to support legacy HTTP servers. Setting this option // will fake `"PATCH"`, `"PUT"` and `"DELETE"` requests via the `_method` parameter and // set a `X-Http-Method-Override` header. Backbone.emulateHTTP = false; // Turn on `emulateJSON` to support legacy servers that can't deal with direct // `application/json` requests ... will encode the body as // `application/x-www-form-urlencoded` instead and will send the model in a // form param named `model`. Backbone.emulateJSON = false; Backbone.sync = function(method, model, options) { var type = methodMap[method]; // Default options, unless specified. _.defaults(options || (options = {}), { emulateHTTP: Backbone.emulateHTTP, emulateJSON: Backbone.emulateJSON }); // Default JSON-request options. var params = {type: type, dataType: 'json'}; // Ensure that we have a URL. if (!options.url) { params.url = _.result(model, 'url') || urlError(); } // Ensure that we have the appropriate request data. if (options.data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { params.contentType = 'application/json'; params.data = JSON.stringify(options.attrs || model.toJSON(options)); } // For older servers, emulate JSON by encoding the request into an HTML-form. if (options.emulateJSON) { params.contentType = 'application/x-www-form-urlencoded'; params.data = params.data ? {model: params.data} : {}; } // For older servers, emulate HTTP by mimicking the HTTP method with `_method` // And an `X-HTTP-Method-Override` header. if (options.emulateHTTP && (type === 'PUT' || type === 'DELETE' || type === 'PATCH')) { params.type = 'POST'; if (options.emulateJSON) params.data._method = type; var beforeSend = options.beforeSend; options.beforeSend = function(xhr) { xhr.setRequestHeader('X-HTTP-Method-Override', type); if (beforeSend) return beforeSend.apply(this, arguments); }; } // Don't process data on a non-GET request. if (params.type !== 'GET' && !options.emulateJSON) { params.processData = false; } // If we're sending a `PATCH` request, and we're in an old Internet Explorer // that still has ActiveX enabled by default, override jQuery to use that // for XHR instead. Remove this line when jQuery supports `PATCH` on IE8. if (params.type === 'PATCH' && noXhrPatch) { params.xhr = function() { return new ActiveXObject("Microsoft.XMLHTTP"); }; } // Pass along `textStatus` and `errorThrown` from jQuery. var error = options.error; options.error = function(xhr, textStatus, errorThrown) { options.textStatus = textStatus; options.errorThrown = errorThrown; if (error) error.apply(this, arguments); }; // Make the request, allowing the user to override any Ajax options. var xhr = options.xhr = Backbone.ajax(_.extend(params, options)); model.trigger('request', model, xhr, options); return xhr; }; Backbone.$ = $; // Set the default implementation of `Backbone.ajax` to proxy through to `$`. // Override this if you'd like to use a different library. Backbone.ajax = function() { return Backbone.$.ajax.apply(Backbone.$, arguments); }; var wrapError = function(model, options) { var error = options.error; options.error = function(resp) { if (error) error(model, resp, options); model.trigger('error', model, resp, options); }; }; })(); </script> </body> </html>