前言
本文分为入门和进阶两部分,建议有经验的读者直接阅读进阶部分。
本文主要参考了vue和on-change两个开源库,若读者阅读过它们的源码可以直接跳过本文 :)
入门
关于Object.defineProperty
首先我们需要知道如何通过Object.defineProperty
这个API来监听一个对象的变化, 注意注释里的内容!
const obj = {};
let val = obj.name;
Object.defineProperty(obj, 'name', {
set(newVal) {
console.warn(newVal);
// 想知道为什么不直接写成obj.name = newVal吗, 自己试试吧 :)
val = newVal;
},
});
setTimeout(() => {
// 一秒钟后我们将obj这个对象的name属性赋值为字符串a, 看看会发生什么
obj.name = 'a';
}, 1000);
复制代码
好了,现在你知道如何通过Object.defineProperty
这个API来监听一个对象的变化了吧,不过你还要注意一些细节
const obj = {};
let val = obj.name;
Object.defineProperty(obj, 'name', {
set(newVal) {
console.warn(newVal);
val = newVal;
},
});
setTimeout(() => {
obj.name = 'a';
// 由于我们没有设置enumerable描述符,所以它是默认值false, 也就是说obj的name属性是无法被枚举的
console.warn(obj);
// 这个很好理解,因为我们没有设置get方法
console.warn(obj.name);
}, 1000);
复制代码
也就是说我们需要加上这些
Object.defineProperty(obj, 'name', {
enumerable: true,
// 想知道为什么要加上configurable描述符吗,试试delete obj.name吧
configurable: true,
get() {
return val;
},
set(newVal) {
console.warn(newVal);
val = newVal;
},
});
复制代码
另外,数组对象是个特例,mutable的原型方法我们无法通过Object.defineProperty
来监听到
const obj = {
val: [],
};
let val = obj.val;
Object.defineProperty(obj, 'val', {
get() {
return val;
},
set(newVal) {
console.warn(newVal);
val = newVal;
},
});
setTimeout(() => {
// 没有任何反应
obj.val.push('b');
}, 1000);
复制代码
因此我们还需要去劫持数组对象mutable的原型方法, 包括push
, pop
, shift
, unshift
, splice
, sort
, reverse
, 我们以push
为例:
const obj = {
val: [],
};
const arrayMethods = Object.create(Array.prototype);
arrayMethods.push = function mutator(...args) {
console.warn(args);
[].push.apply(this, args);
};
// 如果浏览器实现了__proto__, 覆盖原型对象
if ('__proto__' in {}) {
val.__proto__ = arrayMethods;
} else {
// 要是浏览器没有实现__proto__, 覆盖对象本身的该方法
Object.defineProperty(val, 'push', {
value: arrayMethods['push'],
enumerable: true,
});
}
setTimeout(() => {
obj.val.push('b');
}, 1000);
复制代码
好了,以上就是关于如何通过Object.defineProperty
这个API来监听一个对象的变化的全部。
关于Proxy
通过Proxy
来监听对象变化要比Object.defineProperty
容易的多
let obj = {};
obj = new Proxy(obj, {
set(target, prop, newVal) {
console.warn(newVal);
// 你也可以使用Reflect.set()
target[prop] = newVal;
return true;
},
});
setTimeout(() => {
// 一秒钟后我们将obj这个对象的name属性赋值为字符串a
obj.name = 'a';
// 显然我们不需要更多的设置
console.warn(obj);
console.warn(obj.name);
}, 1000);
复制代码
同样的对于数组对象的监听也没有那么多hacky的味道
const obj = {
val: [],
};
obj.val = new Proxy(obj.val, {
set(target, prop, newVal) {
const oldVal = target[prop];
if (oldVal !== newVal) {
console.warn(oldVal, newVal);
}
target[prop] = newVal;
return true;
},
});
setTimeout(() => {
obj.val.push('a');
}, 1000);
复制代码
好了,以上就是关于如何通过Proxy
来监听一个对象的变化的全部。
进阶
关于分类和递归
假如我们现在有这样一个对象obj
, 如何监听它的所有属性呢
let obj = {
b: true,
o: { name: 'obj' },
a: ['a', 'b', 'c'],
odeep: {
path: {
name: 'obj deep',
value: [],
},
},
};
复制代码
我们可以分类讨论,先考虑基本类型的变量以及Object类型的变量
function isPlainObject(obj) {
return ({}).toString.call(obj) === '[object Object]';
}
// 首先先定义一个劫持对象属性的通用函数
function defineReactive(obj, key, val) {
if (isPlainObject(val)) {
observe(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val;
},
set(newVal) {
console.warn(newVal);
val = newVal;
// 赋的新值不为基本类型, 也同样需要劫持
if (isPlainObject(newVal)) {
observe(newVal);
}
},
});
}
// 遍历所有属性并劫持
function observe(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
observe(obj);
setTimeout(() => {
// 显然不会有什么问题
obj.b = false;
obj.o.name = 'newObj';
obj.odeep.path.name = 'newObj deep';
obj.b = { name: 'obj created' };
obj.b.name = 'newObj created';
}, 1000);
复制代码
我们再来考虑Array类型的变量
function defineReactive(obj, key, val) {
if (isPlainObject(val)) {
observe(val);
} else if (Array.isArray(val)) {
dealAugment(val);
observeArray(val);
}
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return val;
},
set(newVal) {
console.warn(newVal);
val = newVal;
if (isPlainObject(newVal)) {
observe(newVal);
} else if (Array.isArray(newVal)) {
dealAugment(newVal);
observeArray(newVal);
}
},
});
}
function dealAugment(val) {
const arrayMethods = Object.create(Array.prototype);
// 我们以push方法为例
arrayMethods.push = function mutator(...args) {
console.warn(args);
[].push.apply(this, args);
};
// 如果浏览器实现了__proto__, 覆盖原型对象
if ('__proto__' in {}) {
obj.val.__proto__ = arrayMethods;
} else {
// 要是浏览器没有实现__proto__, 覆盖对象本身的该方法
Object.defineProperty(obj.val, 'push', {
value: arrayMethods['push'],
enumerable: true,
});
}
}
function observeArray(obj) {
obj.forEach((el) => {
if (isPlainObject(el)) {
observe(el);
} else if (Array.isArray(el)) {
observeArray(el);
}
});
}
observe(obj);
setTimeout(() => {
// 显然不会有什么问题
obj.a.push('d');
obj.odeep.path.value.push(1);
obj.b = ['a'];
obj.b.push('b');
}, 1000);
复制代码
显然,Object.defineProperty
的版本有些冗长,那么Proxy
的版本如何呢?
const handler = {
get(target, prop) {
try {
// 还有比这更简洁的递归吗
return new Proxy(target[prop], handler);
} catch (error) {
return target[prop]; // 或者是Reflect.get
}
},
set(target, prop, newVal) {
const oldVal = target[prop];
if (oldVal !== newVal) {
console.warn(oldVal, newVal);
}
target[prop] = newVal;
return true;
},
};
obj = new Proxy(obj, handler);
setTimeout(() => {
// 试试吧,太不可思议了!
obj.b = false;
obj.o.name = 'newObj';
obj.odeep.path.name = 'newObj deep';
obj.b = { name: 'obj created' };
obj.b.name = 'newObj created';
obj.a.push('d');
obj.odeep.path.value.push(1);
obj.b = ['a'];
obj.b.push('b');
obj.b[0] = 'new a';
}, 1000);
复制代码
以上就是监听一个对象变化的所有内容了。不过细心的你应该发现了,我们使用了console.warn(newVal)
这样强耦合的写法, 下篇文章将会介绍如何使用观察者模式实现类似Vue.prototype.$watch
的功能。