Model(模型)模块在bk框架中的作用主要是存储处理数据,它对外和对内都有很多操作数据的接口和方法。它与视图(Views)模块精密联系着,通过set函数改变数据结构从而改变视图界面的变化。下面我们来看一下它的构造函数:
//传入两个参数,a需要保存的数据,b是该模型所属的colltion,如果没有可以为空 e.Model = function(a, b) { var c; a || (a = {});
//defaults 是默认的属性,在extend该模型的时候可以设置一些默认必选的属性。 if (c = this.defaults) f.isFunction(c) && (c = c.call(this)), a = f.extend({}, c, a); //设置内部对外的默认属性集合 this.attributes = {}; //设置私有属性集合 this._escapedAttributes = {}; //设置模型的cid值,该值唯一,可以作为访问该对象的key this.cid = f.uniqueId("c"); //将数据填充到模型中去,不触发set事件,因为silent 设置是 true this.set(a, { silent: !0 }); //设置默认的_changed值,该值会在set事件作为该模型对象的值是否改变的判断标识。 this._changed = !1; //克隆一份数据给私有变量_previousAttributes. this._previousAttributes = f.clone(this.attributes); //如果你传入b ,就表示这个model属于哪个collection if (b && b.collection) this.collection = b.collection; //执行initialize 初始化函数 this.initialize(a, b) };
Model的构造函数比较简单,理解起来并不复杂,它只是一些属性和数据的初始化。在实际应用中我们可以来试试看怎么样构造一个模型对象:
上面的代码中我们构造了一个简单的模型对象,在那里我们并没有设置一些初始化函数和属性,而是给这个模型对象里面放入了些数据。那么现在可以看看,Model实例化后得到的结果,在浏览器中查看model我们会看到:
它拥有一些已经设置过的属性,并且最重要的是它继承了Model的所有方法。在__proto__里面我们可以看到这些方法的具体应用。下面我们就里面的这些方法做简要的说明:
//这一句我们在Event模块里面有介绍到,将e.Event和其他一些方法属性扩展到model里面去, f.extend(e.Model.prototype, e.Events, { _previousAttributes: null, _changed: !1, idAttribute: "id", //初始化函数 initialize: function() {}, //取出储存在自身的数据,并且转换为json格式的数据,该方法为View提供基础的json数据进行界面渲染。 toJSON: function() { /* 可以看到,这里使用的是underscore.js的clone,通过查阅underscore的clone方法, 我们可以找到clone是对象的克隆,也就是说使用clone方法返回的是开辟了新的内存的对象 之所以这样做,是为了保护this.attributes属性不被外部所篡改,因为bk只为我们提供了一个唯一合法改变model的attributes值的方法,那就是set方法。它不允许有其他的入口非法改变这个值。我们以后可以看到,通过其他接口是可以改变的,但是无法触发相应事件 因为bk没有监听直接修改值的方法,无法更新视图,这样做是违背观察者模式的原理的。所以比如在外部使用ModelA.toJSON().xxx = xxx;这个方法是无法改变this.attributes的。 */ return f.clone(this.attributes) }, /*访问某个属性,事实上我们在这里发现了bk的一个bug,或者说是缺陷:如果此时this.atributes[a]是以一个对象直接量的形式存在(比如{a:2}),那么我们就能通过外部赋值改变它。ModelA.get('xxx').a = 3;此时this.attributes['xxx'].a的值就是3了。*/ get: function(a) { return this.attributes[a] }, //这个方法与get方法相同,同样是返回某个属性的。 escape: function(a) { var b; if (b = this._escapedAttributes[a]) return b; b = this.attributes[a]; return this._escapedAttributes[a] = (b == null ? "": "" + b).replace(/&(?!\w+;|#\d+;|#x[\da-f]+;)/gi, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'").replace(/\//g, "/") }, //检查模型是否有某个属性 has: function(a) { return this.attributes[a] != null }, /* set方法是model中一个经常用到的方法,也是模型驱动视图概念中的一个主要方法。事实上我们可以看到这里采用的是在js设计模式中提到过的一种设计模式(此处说明的不是它的外在关联作用,而是内部设计)这就是:外观模式,也就是说 set本身只是一个对外借口,它实际上是多个接口的集成,构成了它作为一个观察者的角色。这个三个方法我们在下面的代码中说明。 */ set: function(a, b) { b || (b = {}); if (!a) return this; if (a.attributes) a = a.attributes; var c = this.attributes, d = this._escapedAttributes; //判断数据是否验证通过 if (!b.silent && this.validate && !this._performValidation(a, b)) return ! 1; if (this.idAttribute in a) this.id = a[this.idAttribute]; var e = this._changing; this._changing = !0; for (var g in a) { var h = a[g]; //第一个 isEqual 方法是underscore判断传入的对象和本身存储的对象是否相等,追踪underscore源码,可以发现这个方法是一个多次循环判断,它最终匹配的是数字和字符串的值,所以,不要传入多层嵌套对象进去进行对比(jq对象千万不要传!很容易造成内存泄露)。 if (!f.isEqual(c[g], h)) c[g] = h, delete d[g], this._changed = !0, //其次是this.trigger方法,前面已经提到过是触发绑定的事件。在某个属性的值改变是,会触发该方法。我们看到change后面加了一个: g变量,这个g就是属性名。 b.silent || this.trigger("change:" + g, this, h, b) //最后是change方法。这个方法的作用是重置私有属性和一些基础属性。 } ! e && !b.silent && this._changed && this.change(b); this._changing = !1; return this }, //从内部属性散列表中删除指定属性(attribute)。工作原理和set方法基本相似。在此不做说明。 unset: function(a, b) { if (! (a in this.attributes)) return this; b || (b = {}); var c = {}; c[a] = void 0; if (!b.silent && this.validate && !this._performValidation(c, b)) return ! 1; delete this.attributes[a]; delete this._escapedAttributes[a]; a == this.idAttribute && delete this.id; this._changed = !0; b.silent || (this.trigger("change:" + a, this, void 0, b), this.change(b)); return this }, //从model中删除所有属性,工作原理和set方法基本相似。在此不做说明。 clear: function(a) { a || (a = {}); var b, c = this.attributes, d = {}; for (b in c) d[b] = void 0; if (!a.silent && this.validate && !this._performValidation(d, a)) return ! 1; this.attributes = {}; this._escapedAttributes = {}; this._changed = !0; if (!a.silent) { for (b in c) this.trigger("change:" + b, this, void 0, a); this.change(a) } return this }, /* 以下三个方法都本地模型和服务器之间的关联方法。通过zepto或者jq的ajax方法将本地模型序列化后关联到服务器上,然后通过查找,修改,更新,销毁来操作那些数据。这些数据一般不保存在本地*/ //fethc是读数据的方法我们可以看到里面调用了set方法,说明如果读入的模型和不本地的不一致会触发视图改变。 fetch: function(a) { a || (a = {}); var b = this, c = a.success; a.success = function(d, e, f) { if (!b.set(b.parse(d, f), a)) return ! 1; c && c(b, d) }; a.error = i(a.error, b, a); return (this.sync || e.sync).call(this, "read", this, a) }, //save提供的事create和更新的接口。将本地的传到服务器上做保存。 save: function(a, b) { b || (b = {}); if (a && !this.set(a, b)) return ! 1; var c = this, d = b.success; b.success = function(a, e, f) { if (!c.set(c.parse(a, f), b)) return ! 1; d && d(c, a, f) }; b.error = i(b.error, c, b); var f = this.isNew() ? "create": "update"; return (this.sync || e.sync).call(this, f, this, b) }, //摧毁一个服务器的模型,如果你不打算再用它的话。 destroy: function(a) { a || (a = {}); if (this.isNew()) return this.trigger("destroy", this, this.collection, a); var b = this, c = a.success; a.success = function(d) { b.trigger("destroy", b, b.collection, a); c && c(b, d) }; a.error = i(a.error, b, a); return (this.sync || e.sync).call(this, "delete", this, a) }, //返回模型资源在服务器上位置的相对 URL url: function() { var a = k(this.collection) || this.urlRoot || l(); if (this.isNew()) return a; return a + (a.charAt(a.length - 1) == "/" ? "": "/") + encodeURIComponent(this.id) }, parse: function(a) { return a }, //返回该模型的具有相同属性的新实例。 clone: function() { //this.constructor指的就是model的构造函数 return new this.constructor(this); }, //判断Model是否是新建的。 isNew: function() { return this.id == null }, //上文提到过,重置一些基础,触发change事件。使用clone方法,确保两份数据独立开来。 change: function(a) { this.trigger("change", this, a); this._previousAttributes = f.clone(this.attributes); this._changed = !1 }, //判断某个属性是否发生了变化 hasChanged: function(a) { if (a) return this._previousAttributes[a] != this.attributes[a]; return this._changed }, //这个方法也是检测模型的属性是否发生了变化,我们可以看到里面同样是用到了isEqual方法进行验证,所以最好不要传入jq对象去比较验证。 changedAttributes: function(a) { a || (a = this.attributes); var b = this._previousAttributes, c = !1, d; for (d in a) f.isEqual(b[d], a[d]) || (c = c || {}, c[d] = a[d]); return c }, //返回原始值得某个属性。 previous: function(a) { if (!a || !this._previousAttributes) return null; return this._previousAttributes[a] }, //返回一个原始的值,当model的attribute属性发生改变,我们可以通过此方法放回最原始的值。_previousAttributes一直保存的是最初传入的值 previousAttributes: function() { return f.clone(this._previousAttributes) }, //对传入的数据做验证 _performValidation: function(a, b) { //bk本身不提供validate方法,是我们在实例化的时候自己扩展的上去的方法。 var c = this.validate(a); if (c) return b.error ? b.error(this, c, b) : this.trigger("error", this, c, b), !1; return ! 0 } });
这些方法都挂载到了Model的原型链上,通过extend函数我们可以自定义一些基础的方法给它,或者说,单你希望某个内置的方法不如意的时候你可以写一个同名方法覆盖它们,不过除了少数几个方法(比如initialize)外不建议你那样做,除非你希望改写它的整个框架。然后再通过new 关键字实例化一个对象model,于是model得到了Model模块的所有方法。需要特别说明的是,通过var x = Backbone.model.extend()这样的形式扩展的方法只会应用到x的实例化对象上,因为和源码里面的方式不同,一个是在对象上扩展,一个是在prototype原型上扩展。为了验证我们所说的,我们给Model对象上附加自己的方法:
接下来我们再浏览器中控制台中输入model,得到如下结果:
第一个箭头所指的是o也就是Model上的方法,我们可以看到myFunction就在里面,第二个箭头指的是e.model,也就是e.mode原型上的方法,我们可以看到所有其他的没被覆盖的方法都在里面。下面,我们可以看一下这些方法的具体应用:
toJSON方法的应用:获取内部数据;
perviousAttributes();当attributes在外部被改变的时候,可以恢复原始的数据:
接下来我们对一开始实例化函数最一些小小的修改,来测试set方法的效果:
首先,我们扩展了Model对象,并且覆盖了initialize方法,也就是说实例化后立即绑定了change:a方法,即单a属性改变的时候,要求浏览器会输出一段字符串。在set方法里面我们对trigger做了说明,change:a是对a属性的监听。我们执行set函数后会看到如下结果:
Model的核心概念就是数据,它最主要的方法还是存储和处理数据,至于驱动视图这个概念,我们在前面的Event模块中已经提到过了,这个工作是一个各个模块交互的工作,Events,Views和Model都提供了接口来实现该设计模式。
小提示:change:a方法的时候键值和键名之间不能有空格,源码里面做了死规定!当然你可以改改。卤煮曾经被此问题困惑很久。都是吃了没仔细阅读API的大亏。