數據劫持,指的是在訪問或者修改對象的某個屬性時,通過一段代碼攔截這個行為,進行額外的操作或者修改返回結果。
數據劫持經典應用
vue雙向數據綁定
數據劫持常見實現思路
- 利用Object.defineProperty設置 setter及getter
- 利用ES6新增的proxy設置代理
具體實現
defineProperty方式
// 1 定義一個對象
let obj = {
name: 'Bill'
}
// 2 定義監聽函數
function observer(obj) {
if(typeof obj === 'object') {
for (let key in obj) {
// defineReactive 方法設置get和set,見第三步
defineReactive(obj, key, obj[key]);
}
}
}
// 3 定義defineReactive函數處理每個屬性
function defineReactive(obj, key, value) {
Object.defineProperty(obj, key, {
get() {
return value;
},
set(val) {
console.log('數據更新了')
value = val;
}
})
}
// 4 初步實現數據劫持,測試,控制台輸出:數據更新了
observer(obj);
obj.name = 'haha'
// 5 以上已經實現設置obj的屬性的時候,被監聽到,並且可以去執行一些動作了。但是,如果對象里面嵌入了對象呢?如:
let obj = {
name: 'Bill',
age: {
old: 60
}
}
// 6 再次測試,控制台無輸出,額外動作未被執行
observer(obj);
obj.age.old = '50'
// 7 為解決上述問題,對監控的obj進行遞歸迭代處理
function defineReactive(obj, key, value) {
// 如果對象的屬性也是一個對象。迭代處理
observer(value);
Object.defineProperty(obj, key, {
//....
})
}
// 8 再次測試,輸出 數據更新了
observer(obj);
obj.age.old = '50'
// 9 但是,到這一步,仍不完善,當obj為數組(也是對象)時將仍然無法在通過內置方法改變數組時觸發行為
// 為obj新增一個屬性,屬性值為數組,為該數組增加元素,控制台無輸出
obj.skill = [1, 2, 3];
obj.skill.push(4);
// 10 重寫數組內置方法,實現改變數組元素時觸發行為
let arr = ['push', 'pop', 'splice','shift', 'unshift'];
arr.forEach(method=> {
let oldPush = Array.prototype[method];
Array.prototype[method] = function(value) {
console.log('數據更新了')
oldPush.call(this, value)
}
})
// 11 再次測試 控制台輸出 數據更新了。數據劫持基本實現
obj.skill = [1, 2, 3];
obj.skill.push(4);
以下為完整代碼:
let obj = {
name: 'Bill',
age: {
old: 60
},
skill:[1,2,3]
}
// vue 數據劫持 Observer.defineProperty
function observer(obj) {
if(typeof obj === 'object') {
for (let key in obj) {
defineReactive(obj, key, obj[key]);
}
}
}
function defineReactive(obj, key, value) {
observer(value);
Object.defineProperty(obj, key, {
get() {
return value;
},
set(val) {
console.log('數據更新了')
value = val;
}
})
}
observer(obj);
// 重寫數組相關方法
let arr = ['push', 'pop', 'splice','shift', 'unshift'];
arr.forEach(method=> {
let oldPush = Array.prototype[method];
Array.prototype[method] = function(value) {
console.log('數據更新了')
oldPush.call(this, value)
}
})
// 以下為測試 輸出兩次 數據更新了
obj.age.old = 50
obj.skill.push(40)
ES6 Proxy方式
// 判斷傳入的數據是否為數組
function isArray(o){
return Object.prototype.toString.call(o) === `[object Array]`
}
// 判斷傳入數據是否為普通對象
function isObject(o){
return Object.prototype.toString.call(o) === `[object Object]`
}
class Observer{
constructor(
target,
handler = {
set(target, key, value, receiver){
console.log('檢測到了set的key為 -> ' + key);
return Reflect.set(target, key, value, receiver);
}
}
){
if( !isObject(target) && !isArray(target) ){
throw new TypeError('target 不是數組或對象')
}
this._target = JSON.parse(JSON.stringify(target)); // 避免引用修改 數組不考慮
this._handler = handler;
return new Proxy(this._observer(this._target), this._handler);
}
// 為每一項為Array或者Object類型數據變為代理
_observer(target){
// 遍歷對象中的每一項
for( const key in target ){
// 如果對象為Object或者Array
if( isObject(target[key]) || isArray(target[key]) ){
// 遞歸遍歷
this._observer(target[key]);
// 轉為Proxy
target[key] = new Proxy(target[key], this._handler);
}
}
// 將轉換好的target返回出去
return target;
}
}
// 利用以上封裝好的Observer類 實現數據劫持
const o = {
a : [1, 2],
c : {
a : 1,
b : 2,
c : [
[1,2,{
d : 3
}]
]
},
b : 2
}
const ob = new Observer(o);
ob.a.push(3); // 檢測到了set的key為 -> 2 檢測到了set的key為 -> length
ob.c.a = 2; // 檢測到了set的key為 -> a
ob.c.c[0][2].d = 6; // 檢測到了set的key為 -> d
ob.b = 44; // 檢測到了set的key為 -> b
總結
ES5 defineProperty方式及ES6 Proxy方式均能實現數據劫持,其中前者是Vue2數據劫持實現方式。
相比較而言,ES6 Proxy方式更優秀,能同時監聽到對象及數組的變化,不需要重寫數組方法,它也是Vue3實現數據劫持所采用方案。