论如何监听一个对象所有属性的变化
前言
本文分为入门和进阶两部分,建议有经验的读者直接阅读进阶部分。
本文主要参考了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属性赋值为字符串aobj.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的功能。
转载于:https://juejin.im/post/5cc68feef265da036c57940a
总结
以上是生活随笔为你收集整理的论如何监听一个对象所有属性的变化的全部内容,希望文章能够帮你解决所遇到的问题。
- 上一篇: SharePreference源码学习和
- 下一篇: JAVA-接口和抽象类的区别