1.我们要做什么
实现一个迷你库,检测一组数据的变化,当数据变化的时候,做出响应。大致像下面的使用方式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var callback = function(newVal, oldVal){ alert(newVal + '----' + oldVal); };
var data = { a: 200, level1: { b: 'str', c: [1, 2, 3], level2: { d: 90 } } }
var v = new Mvvm(data, callback);
|
怎么实现?
2.Object.defineProperty()
我们可以使用下面的方式定义一个对象,并给对象添加一个属性。
1 2 3 4
| var obj = ;
obj.name = 'hcy';
|
也可以使用Object.defineProperty()给对象添加属性,上面的代码等价于
1 2 3 4 5 6 7 8
| var obj = {};
Object.defineProperty(obj, 'name', { value: 'hcy', writable: true, enumerable: true, configurable: true })
|
这样定义的属性是数据属性,用来描述数据。还有另外一个访问器属性,允许你定义一对儿 getter/setter ,当你读取属性值的时候底层会调用 get 方法,当你去设置属性值的时候,底层会调用 set 方法。
这简直太美妙了,这不正是我们想要的吗?
遍历对象的属性,把对象的属性都使用 Object.defineProperty 转为 getter/setter ,这样,当我们修改一些值得时候,就会调用set方法,然后我们在set方法里面,回调通知,这样不就可以了吗?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const OP = Object.prototype;
class Mvvm{ constructor(obj, callback){ if(OP.toString.call(obj) !== '[object Object]'){ console.error('This parameter must be an object:' + obj); } this.$callback = callback; this.observe(obj); } observe(obj){ Object.keys(obj).forEach(function(key, index, keyArray){ var val = obj[key]; Object.defineProperty(obj, key, { get: function(){ return val; }, set: (function(newVal){ this.$callback(newVal); }).bind(this) }); if(OP.toString.call(obj[key]) === '[object Object]'){ this.observe(obj[key]); } }, this); } }
|
测试一下我们的代码
3.保存旧值
现在我们成功的监测到数据变化并且做出响应了。但是这个响应只处理了新值,有时候我们是需要旧值的,现在我们的代码还无法传递旧值,怎么改?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| var oldVal = obj[key]; Object.defineProperty(obj, key, { get: function(){ return oldVal; }, set: (function(newVal){ if(oldVal !== newVal){ if(OP.toString.call(newVal) === '[object Object]'){ this.observe(newVal); } this.$callback(newVal, oldVal); oldVal = newVal; } }).bind(this) });
|
看下效果
4.监测数组变化
这样子看上去还不错了哦,我们能够监测到数据的变化,拿到了新旧数据。但是这样就完了吗?
遗憾的是我们还不能监测到数组的变化。比如:
有什么思路可以解决这个问题?
重写原型方法可以吗?像是这样:
1 2 3 4 5 6
| Array.prototype.push = function(){ } Array.prototype.pop = function(){ }
|
暂且不说能不能全部实现的与原生无异,即使你实现的与原生方法在使用方式上一模一样,并且不影响其他代码的运行,那么在性能上,可能就与原生差很多了,重新考虑,我们可不可以在原型链中加一层fakePrototype,在使用 push 等数组方法的时候,调用的是 fakePrototype 上的push方法,然后在 fakePrototype 方法中再去调用真正的Array原型上的 push 方法,同时监听变化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
|
const OP = Object.prototype;
const OAM = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
class Mvvm{ constructor(obj, callback){ if(OP.toString.call(obj) !== '[object Object]'){ console.error('This parameter must be an object:' + obj); } this.$callback = callback; this.observe(obj); } observe(obj){ if(OP.toString.call(obj) === '[object Array]'){ this.overrideArrayProto(obj); } Object.keys(obj).forEach(function(key, index, keyArray){ var oldVal = obj[key]; Object.defineProperty(obj, key, { get: function(){ return oldVal; }, set: (function(newVal){ if(oldVal !== newVal){ if(OP.toString.call(newVal) === '[object Object]' || OP.toString.call(newVal) === '[object Array]'){ this.observe(newVal); } this.$callback(newVal, oldVal); oldVal = newVal; } }).bind(this) }); if(OP.toString.call(obj[key]) === '[object Object]' || OP.toString.call(obj[key]) === '[object Array]'){ this.observe(obj[key]); } }, this); } overrideArrayProto(array){ var originalProto = Array.prototype, overrideProto = Object.create(Array.prototype), self = this, result; Object.keys(OAM).forEach(function(key, index, array){ var method = OAM[index], oldArray = []; Object.defineProperty(overrideProto, method, { value: function(){ oldArray = this.slice(0); var arg = [].slice.apply(arguments); result = originalProto[method].apply(this, arg); self.observe(this); self.$callback(this, oldArray); return result; }, writable: true, enumerable: false, configurable: true }); }, this); array.__proto__ = overrideProto; } }
|
看下效果
5.模板映射
现在,我们可以几乎完美的监测到数据对象的变化了,并且能够知道变化前后的旧值与新值,那么这样就结束了吗?当然不是,我们现在并不知道修改的是哪个属性,但是能够知道修改的是哪个属性是非常重要的。比如,我们有如下的模板:
1 2 3 4 5
| <div id="box"> <div>{{name}}</div> <div>{{age}}</div> <div>{{sex}}</div> </div>
|
然后有如下的数据:
1 2 3 4
| var data = { name : 'nuoka', age : 20, }
|
我们想通过这种方式反应模板与数据之间的关系:
1 2
| new Mvvm(document.getElementById('box'), data);
|
那么当我们的数据模型data中的某个属性改变的时候,比如 data.name = ‘kage’,如若我们不知道改变的字段名称,那么我们就无法得知要刷新哪部分模板。
那么这里就有一个路径的概念,所谓路径,就是变化的字段的路径,比如有如下数据模型:
1 2 3 4 5 6 7
| var data = { a : { b : { c : 'hcy' } } }
|
那么字段 a 的路径就是用 data.a ,b 的路径就是 data.a.b,c 的路径就是 data.a.b.c,这里我们用数组来表述路径可以是这样:
1 2 3
| 1、 a 的路径是 [‘data’, ‘a’] 2、 b 的路径是 [‘data’, ‘a’, ‘b’] 3、 c 的路径是 [‘data’, ‘a’, ‘b’, ‘c’]
|
有了这个概念之后,我们来修改之前的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75
| observe(obj, path){ // 如果发现 监测的对象是数组的话就要调用 overrideArrayProto 方法 if(OP.toString.call(obj) === '[object Array]'){ this.overrideArrayProto(obj, path); } Object.keys(obj).forEach(function(key, index, keyArray){ var oldVal = obj[key]; var pathArray = path && path.slice(0); if (pathArray) { pathArray.push(key); } else { pathArray = [key]; } Object.defineProperty(obj, key, { get: function(){ return oldVal; }, set: (function(newVal){ if(oldVal !== newVal){ if(OP.toString.call(newVal) === '[object Object]' || OP.toString.call(newVal) === '[object Array]'){ this.observe(newVal, pathArray); } this.$callback(newVal, oldVal, pathArray); oldVal = newVal; } }).bind(this) }); if(OP.toString.call(obj[key]) === '[object Object]' || OP.toString.call(obj[key]) === '[object Array]'){ this.observe(obj[key], pathArray); } }, this); } overrideArrayProto(array, path){ // 保存原始 Array 原型 var originalProto = Array.prototype, // 通过 Object.create 方法创建一个对象,该对象的原型就是Array.prototype overrideProto = Object.create(Array.prototype), self = this, result; // 遍历要重写的数组方法 Object.keys(OAM).forEach(function(key, index, array){ var method = OAM[index], oldArray = []; // 使用 Object.defineProperty 给 overrideProto 添加属性,属性的名称是对应的数组函数名,值是函数 Object.defineProperty(overrideProto, method, { value: function(){ oldArray = this.slice(0); var arg = [].slice.apply(arguments); // 调用原始 原型 的数组方法 result = originalProto[method].apply(this, arg); // 对新的数组进行监测 self.observe(this, path); // 执行回调 self.$callback(this, oldArray, path); return result; }, writable: true, enumerable: false, configurable: true }); }, this); // 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto array.__proto__ = overrideProto; }
|
最后再尝试下修改数组属性