首先,js中的属性分为俩种,一种是数据属性,一种是访问器属性。
var data = {};
data.name = '田二黑';
上面这种就是数据属性。当然和下面效果一样:
Object.defineProperty(obj, 'name', {
value: '田二黑', // 属性的值
writable: true, // 是否可写
enumerable: true, // 是否能够通过for in 枚举
configurable: true // 是否可使用 delete删除
})
当然我们可以定义访问器属性 get set,当你读取age属性时,会自动调用get,设置属性时会调用set
Object.defineProperty(obj, 'age', {
get: function(){
return 20;
},
set: function(newVal){
this.age += 20;
}
})
其中,vue就是利用访问器实现的数据双向绑定,像下面这个例子(可能你家没满月的孩子都会写了)
new Vue({
data:{
name:'田二黑',
age:21
}
})
如果我们把data对象的属性全部转化为访问器属性,那我们不就可以检测变化了,修改时候会调用set访问器,在里面回调通知不就行了?
const OP = Object.prototype;
const types = {
obj:'[object Object]',
array:'[object Array]'
}
export default class Jsonob{
constructor(obj,cb){
if(OP.toString.call(obj) !== types.obj){
console.log('请传入一个对象');
return false;
}
this._callback = cb;
this.observe(obj);
}
observe(obj){
Object.keys(obj).forEach((key)=>{
let val = obj[key];
Object.defineProperty(obj,key,{
get:function(){
return val;
},
set:(function(newVal){
this._callback(newVal)
val = newVal
}).bind(this)
})
},this)
}
}
上面代码声明了类Jsonob,接收要监听的对象和回调函数;observe方法,遍历该对象,并依次将对象属性转为访问器属性,在set中回调通知。
接下来我们测试一下
import Jsonob from './jsonOb'
var data = {
a: 200,
level1: {
b: 'str',
c: [1, 2, 3],
level2: {
d: 90
}
}
}
var cb = (val)=>{
console.log(val)
}
new Jsonob(data,cb);
data.level1.level2.d = 50
当修改对象data中属性时,回调打印出新的值。这样还没结束,我的旧值去哪了,我想获取旧值咋办?并且如果我设置的新值又是个对象咋办
let val = obj[key];
Object.defineProperty(obj,key,{
get:function(){
return val;
},
set:(function(newVal){
this._callback(newVal)
val = newVal
}).bind(this)
})
上面的val = obj[key];存储的不就是旧值吗?于是修改代码如下
Object.keys(obj).forEach((key)=>{
let 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)
})
if(OP.toString.call(obj[key]) === types.obj){
this.observe(obj[key])
}
},this)
判断修改的值是否为对象,如果是对象,则继续转换新增的值的属性为访问器属性。在回调中就能接收新值和旧值。当然相信你已经发现了,
data.leavel.c是个数组,当我们push,shift等操作时还监听不到,首先,当我们调用数组的push等方法时,是执行的数组原型上的方法,那我们重
写原型上的这些方法,在这些方法里面监听不就ok了,像这样
Array.prototype.push = function(){
/********/
Array.prototype.shift= function(){
/********/
}
数组有push,shift,pop,unshift等等,你要重写那么多方法并实现其功能,就算你实现了,并且不影响其他代码中数组的使用,性能上来说也是不
能相提并论的。那我们怎么实现?我们可不可以让数组实例的原型指向一个我们自定义的对象fakeprototype,当我们调用push方法时,调用的是该
对象上的push方法,在方法里面监听变化,然后在调用Array.prototype真正原型对象上的push方法不就行了。代码实现如下:
const OP = Object.prototype;
const types = {
obj:'[object Object]',
array:'[object Array]'
}
const OAM =['push','pop','shift','unshift','short','reverse','splice']
export default class Jsonob{
constructor(obj,cb){
if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){
console.log('请传入一个对象或数组');
return false;
}
this._callback = cb;
this.observe(obj);
}
observe(obj){
if(OP.toString.call(obj) === types.array){
this.overrideArrayProto(obj);
}
Object.keys(obj).forEach((key)=>{
let 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)
})
if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.array){
this.observe(obj[key])
}
},this)
}
overrideArrayProto(array){
// 保存原始 Array 原型
var originalProto = Array.prototype,
// 通过 Object.create 方法创建一个对象,该对象的原型是Array.prototype
overrideProto = Object.create(Array.prototype),
self = this,
result;
// 遍历要重写的数组方法
OAM.forEach((method)=>{
Object.defineProperty(overrideProto,method,{
value:function(){
var oldVal = this.slice();
//调用原始原型上的方法
result = originalProto[method].apply(this,arguments);
//继续监听新数组
// self.observe(this);
self._callback(this,oldVal);
return result;
}
})
});
// 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto
array.__proto__ = overrideProto;
}
}
当我们再去对data.leave1.c.push()的时候,就能监听到变化。然而还没有完,我们现在只是知道了修改的新值和旧值,我们修改的哪个属性啊?我们
现在的程序还无法知道,像vue,在模板中
性的好像,不然只能对模板重新全部刷新,性能肯定是不如局部修改的。因此我们还要在代码的基础上加个路径变量,表示是data的哪个属性。
const OP = Object.prototype;
const types = {
obj:'[object Object]',
array:'[object Array]'
}
const OAM =['push','pop','shift','unshift','short','reverse','splice']
export default class Jsonob{
constructor(obj,cb){
if(OP.toString.call(obj) !== types.obj && OP.toString.call(obj) !== types.array){
console.log('请传入一个对象或数组');
return false;
}
this._callback = cb;
this.observe(obj);
}
observe(obj,path){
if(OP.toString.call(obj) === types.array){
this.overrideArrayProto(obj,path);
}
Object.keys(obj).forEach((key)=>{
let oldVal = obj[key];
let pathArray = path&&path.slice();
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]'){
this.observe(newVal,pathArray);
}
this._callback(newVal,oldVal,pathArray)
oldVal = newVal
}
}).bind(this)
})
if(OP.toString.call(obj[key]) === types.obj || OP.toString.call(obj[key]) === types.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;
// 遍历要重写的数组方法
OAM.forEach((method)=>{
Object.defineProperty(overrideProto,method,{
value:function(){
var oldVal = this.slice();
//调用原始原型上的方法
result = originalProto[method].apply(this,arguments);
//继续监听新数组
self.observe(this,path);
self._callback(this,oldVal,path);
return result;
}
})
});
// 最后 让该数组实例的 __proto__ 属性指向 假的原型 overrideProto
array.__proto__ = overrideProto;
}
当第一次调用observe时,path为空,则pathArray将当前key传入,如果不为空,则继续追加path。好了,我们现在的程序算是比较完整了,知道
要修改的属性,新值和就旧值。