前言
介紹雙向綁定,數(shù)據(jù)劫持的文章有很多,作為前端圈的熱點話題,我就不跟風寫同質(zhì)化的文章了,今天只是把發(fā)現(xiàn)的一些不容易被注意到的現(xiàn)象寫出來。
方案一
核心:在 get 方法中進行遞歸調(diào)用
const target = {
name: 'lee',
info: {
age: 24
}
};
const isObj = (data) => {
return Object.prototype.toString.call(data) === '[object Object]'
}
const handler = {
get(trapTarget,key,receiver) {
if (!(key in receiver)) {
throw new Error(`Property ${key} does not exist.`);
}
console.log(`監(jiān)聽到了${key}`)
// 遞歸調(diào)用
if (isObj(trapTarget[key])) {
return new Proxy(trapTarget[key], handler);
}
return Reflect.get(trapTarget,key,receiver);
},
set(trapTarget,key,value,receiver) {
console.log(`修改了${key}`)
return Reflect.set(trapTarget,key,value,receiver);
}
};
const observe = (data) => {
if (!data || !isObj(data)) {
return false;
}
return new Proxy(data, handler);
}
測試:
let proxyData = observe(target);
console.log(proxyData.name);
proxyData.info.age = 30;
console.log(proxyData.info.age);

這種方法確實實現(xiàn)了對深層次數(shù)據(jù)的監(jiān)聽,但是仔細觀察會發(fā)現(xiàn)如下現(xiàn)象:
- info 這個對象并不是 proxy 實例
- 當 get 被觸發(fā)時,get 方法中的遞歸運算總是被執(zhí)行
我們本來的期望是,在經(jīng)過第一次的轉(zhuǎn)化為 proxy 后,以后不再執(zhí)行,但事實顯然不是這樣的。
這里發(fā)現(xiàn)第一個小坑:get 方法中的返回值并不會改變 proxy 實例。
如果你更細心的思考,會發(fā)現(xiàn)我這句結(jié)論來的有些突然,因為你會質(zhì)疑我是因為 if 判斷語句總是為 true 才導致上面的現(xiàn)象,那請你和我繼續(xù)探究。
if (isObj(trapTarget[key])) {
return new Proxy(trapTarget[key], handler);
}
開始我也覺得是因為判斷條件的問題,所以可以加上對 proxy 實例的判斷,如果已經(jīng)是 proxy ,那么不再執(zhí)行,修改后是下面這樣:
if (isObj(trapTarget[key]) && !isProxy(key)) {
console.log(`執(zhí)行了幾次${isProxy(key)}`)
const res = new Proxy(trapTarget[key], handler);
proxies[key] = 1;
return res;
}
說說如何實現(xiàn) isProxy ?
原理就是將轉(zhuǎn)換過的 key收集起來,之后判斷是否存在。
let proxies = {};
const isProxy = (data) => {
return proxies[data];
}
推薦閱讀 es6-proxies.html, 其中的實現(xiàn)方法是下面這樣子的:
let proxies = new WeakSet();
export function createProxy(obj) {
let handler = {};
let proxy = new Proxy(obj, handler);
proxies.add(proxy);
return proxy;
}
export function isProxy(obj) {
return proxies.has(obj);
}
運行結(jié)果:

這里發(fā)現(xiàn)確實按照我們的期望,僅執(zhí)行了一次,但是請你仔細對比圖一和圖二,你會發(fā)現(xiàn)只能監(jiān)聽到 info ,而無法監(jiān)聽到 age。
通過以上現(xiàn)象,可以發(fā)現(xiàn),info 仍然是普通對象,get 中只是臨時獲取了 info 的 proxy 實例。
總結(jié):
- 不推薦使用這種方法:
因為這種方式的監(jiān)聽是在操作數(shù)據(jù)時,才會對數(shù)據(jù)臨時進行 proxy 轉(zhuǎn)換,然后才能夠監(jiān)聽到,而且總要執(zhí)行 proxy 轉(zhuǎn)化操作,這太消耗性能了。
方案二
核心:初始就遞歸所有屬性,將深層次的是對象類型(數(shù)組類型可自行拓展)轉(zhuǎn)換成 proxy 實例,這樣只需執(zhí)行一次。
const target = {
name: 'lee',
info: {
age: 24
}
};
const isObj = (data) => {
return Object.prototype.toString.call(data) === '[object Object]'
}
const handler = {
get(trapTarget,key,receiver) {
if (!(key in receiver)) {
throw new Error(`Property ${key} does not exist.`);
}
console.log(`監(jiān)聽到了${key}`)
return Reflect.get(trapTarget,key,receiver);
},
set(trapTarget,key,value,receiver) {
console.log(`修改了${key}`)
return Reflect.set(trapTarget,key,value,receiver);
}
};
let proxyData = {}
const observe = (data, key) => {
if (!data || !isObj(data)) {
return false;
}
if (key) {
proxyData[key] = new Proxy(data, handler);
} else {
// 初始化
proxyData = new Proxy(data, handler);
}
Object.keys(data).forEach(child => {
if (isObj(data[child])) {
observe(data[child], child)
}
})
}
observe(target);
console.log(proxyData.name);
proxyData.info.age = 30;
console.log(proxyData.info.age);
運行結(jié)果:

觀察運行結(jié)果,可以發(fā)現(xiàn)基本上滿足了我們的期望,只是有個小小的缺陷,請看綠色方框標注的地方,此時屬于初始化過程中,起始我們并不關心此時的數(shù)據(jù)變化,所以還要增加個狀態(tài)判斷。
遞歸執(zhí)行完 observe 后,將狀態(tài)變成 false 即可。
if (!isInit) {
console.log(`修改了${key}`)
}
方案三
基本和方案二是相似的,唯一的區(qū)別是,直接更改了原始數(shù)據(jù)。這里不建議使用此種方法,除非應用 revoked 取消代理。