都因为 IE8 不支持 Object.defineProperty
,但是业务还不能脱离 IE7 和 IE8,故研究下 Backbone.Model 的实现机制,找机会给主流的 MVVM 框架补丁
先来看看 Model 的构造函数
var Model = Backbone.Model = function(attributes, options) { var attrs = attributes || {}; options || (options = {}); // 钩子 this.preinitialize.apply(this, arguments); // 每个实例分配一个唯一的 cid this.cid = _.uniqueId(this.cidPrefix); // 数据对象 this.attributes = {}; // 不重要的内容 if (options.collection) this.collection = options.collection; if (options.parse) attrs = this.parse(attrs, options) || {}; // 获取预设在 defaults 字段中的初始键值对或匿名函数 // 这里使用 _.result() 来兼容函数和对象两种类型 var defaults = _.result(this, 'defaults'); // 避免 attrs 中的 undefined 值覆盖掉 defaults 中的默认值 attrs = _.defaults(_.extend({}, defaults, attrs), defaults); // 初始化赋值 this.set(attrs, options); this.changed = {}; // 钩子 this.initialize.apply(this, arguments); };
很简单的代码,做了一些初始化赋值的事情。
用到了一个小技巧 attrs = _.defaults(_.extend({}, defaults, attrs), defaults);
来防止误传入的 undefined 覆盖掉默认的 defaults 值。
Backbone 的精粹都在 set(){}
这个函数里面。
set: function(key, val, options) { if (key == null) return this; // 统一 'key', 'val' 和 {key:val} 这两种形式 // attrs 最终为变动的对象 var attrs; if (typeof key === 'object') { attrs = key; options = val; } else { (attrs = {})[key] = val; } options || (options = {}); // 规则验证. if (!this._validate(attrs, options)) return false; var unset = options.unset; var silent = options.silent; var changes = []; var changing = this._changing; this._changing = true; // 预留上一次的值 if (!changing) { this._previousAttributes = _.clone(this.attributes); this.changed = {}; } // 备份一个当前的数据对象 var current = this.attributes; var changed = this.changed; var prev = this._previousAttributes; // 遍历传入的新数据对象 for (var attrin attrs) { val = attrs[attr]; // 如果新数据与当前不一致,则标记变动的键名 if (!_.isEqual(current[attr], val)) changes.push(attr); // 如果新数据与旧数据不一致,则更新已变动的数据备份 if (!_.isEqual(prev[attr], val)) { changed[attr] = val; } else { delete changed[attr]; } // 真正干活的代码,更新数据或者删除数据 unset ? delete current[attr] : current[attr] = val; } // 更新 id 字段 if (this.idAttributein attrs) this.id = this.get(this.idAttribute); if (!silent) { if (changes.length) this._pending = options; // 遍历变动清单,并且逐个触发 `change:` 事件 for (var i = 0; i < changes.length; i++) { this.trigger('change:' + changes[i], this, current[changes[i]], options); } } if (changing) return this; if (!silent) { // 触发一个总的 `change` 事件 // 注释说这里用 while 是确保嵌套场景也只触发一个 `change` 事件 while (this._pending) { options = this._pending; this._pending = false; this.trigger('change', this, options); } } this._pending = false; this._changing = false; return this; },
整个 set 里面,实际干活的就是 unset ? delete current[attr] : current[attr] = val;
。
没看明白 this._changing
和 this._pending
的使用场景,感觉是一个当多个 set 同时执行时候的一个标记位,但是 JS 是单线程执行,里面又都是 for 语句,按理说可以不用这两个标记位。又或者是我的理解有误。
看到这,给各种Observer打补丁就有了可行性,支持 Object.defineProperty
就用 Object.defineProperty
,不支持的则降级到走 Backbone 的这种 for in
方式。