前言
介紹雙向綁定,數(shù)據(jù)劫持的文章有很多,作為前端圈的熱點(diǎn)話題,我就不跟風(fēng)寫同質(zhì)化的文章了,今天只是把發(fā)現(xiàn)的一些不容易被注意到的現(xiàn)象寫出來。
方案一
核心:在 get 方法中進(jìn)行遞歸調(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);

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

這里發(fā)現(xiàn)確實(shí)按照我們的期望,僅執(zhí)行了一次,但是請你仔細(xì)對比圖一和圖二,你會發(fā)現(xiàn)只能監(jiān)聽到 info ,而無法監(jiān)聽到 age。
通過以上現(xiàn)象,可以發(fā)現(xiàn),info 仍然是普通對象,get 中只是臨時(shí)獲取了 info 的 proxy 實(shí)例。
總結(jié):
- 不推薦使用這種方法:
因?yàn)檫@種方式的監(jiān)聽是在操作數(shù)據(jù)時(shí),才會對數(shù)據(jù)臨時(shí)進(jìn)行 proxy 轉(zhuǎn)換,然后才能夠監(jiān)聽到,而且總要執(zhí)行 proxy 轉(zhuǎn)化操作,這太消耗性能了。
方案二
核心:初始就遞歸所有屬性,將深層次的是對象類型(數(shù)組類型可自行拓展)轉(zhuǎn)換成 proxy 實(shí)例,這樣只需執(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);
運(yùn)行結(jié)果:

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