前言
本文分為入門和進階兩部分,建議有經驗的讀者直接閱讀進階部分。
本文主要參考了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 {}) {
obj.val.__proto__ = arrayMethods;
} else {
// 要是瀏覽器沒有實現__proto__, 覆蓋物件本身的該方法
Object.defineProperty(obj.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
和計算屬性的功能。