论如何监听一个对象所有属性的变化
前言
本文分為入門和進階兩部分,建議有經驗的讀者直接閱讀進階部分。
本文主要參考了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-接口和抽象类的区别