proxy简介
proxy
一般用于库的封装设计,非必要不建议用在业务代码里,因为它会彻底改变数据的行为,很反常识
let obj = {
a: "1",
};
let proxy = new Proxy(obj, {
get(obj, key) {
console.log(obj, key);
return obj[key];
},
defineProperty() {
console.log(arguments);
},
});
控制台执行
obj.a
输出 1 不打印proxy.a
打印内容并输出 1Object.defineProperty(proxy, "a", {value:10})
打印函数的参数
这里可以看出原对象的 get
不会走进 proxy
因此,我们也可以把 proxy
不理解成代理,而是当作一个完全自定义的对象或者类或者工具库
proxy
的使用方式和 promise
类似
function a() {
return new Promise();
}
function b() {
return new Proxy();
}
很少像示例那样直接 new proxy
后赋值给个变量
数据双向绑定示例
React
// 数据
const [data, setData] = useState({ count: 1 }); // reactive()
// 观测变化 需要手动声明依赖(手动声明这个回调对应什么的ey)
useEffect(() => console.log("count changed", data.count), [data.count]);
// 触发 console.log('count changed', data.count) 重新执行
// 手动通过工具触发修改值,而不是直接修改
setData({
count: 2,
});
Vue3
const data = reactive({ count: 0 }); // 定义一个代理后的数据
effect(() => console.log("count changed", data.count)); // 定义回调,代理数据变化时触发
data.count = 2; // 修改代理后的值
👆 我们可以看到 vue3
相比于 vue2
,需要手动把期望响应式的数据放到 reactive
里
vue3
相比于 reactive
,又简化了很多步骤,甚至像个无法理解的黑盒
基于Proxy实现数据双向绑定
需要实现的效果如👇
let dummy; // 假数据
const counter = reactive({ num: 0 }); // 定义一个代理后的数据
effect(() => (dummy = counter.num)); // 定义回调,代理数据变化时触发
// dummy = counter.num; // 直接赋值的形式不会双向绑定
console.log(dummy); // 0
counter.num = 7; // 修改代理后的值
console.log(dummy); // 7
1. 实现基础get、set代理
(function () {
let dummy;
function reactive(data) {
return new Proxy(data, {
get(originData, key) {
console.log("触发get", key);
return originData[key];
},
set(originData, key, newVal) {
console.log("触发set", key);
return (originData[key] = newVal);
},
});
}
const myData = { num: 1 }; // 原数据
const proxyData = reactive(myData); // 代理后数据
// 因为代理的get和set处理好了 修改原对象和修改代理对象 再访问原对象或者代理对象都能是新的值
dummy = proxyData.num;
console.log(dummy); // 1
proxyData.num = 2;
console.log(dummy); // 1
})();
👆 代理后的数据赋值给 dummy
,dummy
不会随着代理数据变化而变化
但是使用数据的时候使用代理对象不就好了吗,为什么要赋值出来使用 看起操作的数据是外层的,不用关心数据是不是在代理对象里层?
2. set时触发订阅者的回调
要实现修改代理对象数据,能通知到赋值的变量,也就是 set
的时候再触发一次赋值(回调) 因此赋值操作需要立即执行一次,以及传递到 set
的回调时执行一次,用 effect
封装起来
(function () {
let dummy;
const callbackList = [];
function reactive(data) {
return new Proxy(data, {
get(originData, key) {
console.log("触发get", key);
return originData[key];
},
set(originData, key, newVal) {
console.log("触发set", key);
originData[key] = newVal;
callbackList.forEach((fn) => fn());
return newVal;
},
});
}
const myData = { num: 1 }; // 原数据
const proxyData = reactive(myData); // 代理后数据
function effect(callback) {
callback(); // 1. 立即执行一次
callbackList.push(callback); // 2. 传递给set的时候执行一次
// 因为代理整个对象所以需要多个不同观察者,存储层数组,在set的时候全部执行(性能浪费)
}
effect(() => (dummy = proxyData.num)); // 触发立即执行回调中赋值的get
console.log(dummy); // 1 不触发proxy的get set
proxyData.num = 2; // 触发set 和回调中的赋值的get
console.log(dummy); // 2 不触发proxy的get set
/* effect(()=>{
console.log('触发num的回调');
dummy = proxyData.num
}); // 触发立即执行回调中赋值的get
effect( ()=>{
console.log('触发num2的回调')
dummy2 = proxyData.num2
} ); // 触发立即执行回调中赋值的get
console.log(dummy); // 1 不触发proxy的get set
proxyData.num = 2; // 触发set 和回调中的赋值的get
proxyData.num2 = 2; // 触发set 和回调中的赋值的get
console.log(dummy); // 2 不触发proxy的get set*/
})();
👆 假如代理多个 reactive
对象,每个对象中多个属性,都共用一个 callback
列表,并且在 set
时触发所有的回调,性能浪费
理想状态是,set
具体的 reactive
对象的具体 key
触发相应的回调
3. set匹配到相应的回调来触发
我们看到现在收集回调的地方,是在立即执行时存起来,只要想办法不是单纯的往回调列表中 push
而是存储对象 key value
,即可在 set
的时候调用相应的回调 但是回调函数是没有明显的 key
给我们定义的
因为要定位到是哪个 reactive
对象和 key
,因此理想的回调map数据结构
如下
const callbackMap = new Map([
[ originObj1, new Map([ [key1: callbackList], [key2: callbackList] ... ])]
[ originObj2, new Map([ [key1: callbackList], [key2: callbackList] ... ])]
])
我们再看一下整个流程,不想污染外部,又不想在外部额外调用工具来存储回调,发现只有 proxy
内部可以获取到 originObj
和 key
那我们利用立即执行回调时,触发 get
的时机,存储回调 (有点类似 vue2 的 computed
,依赖到的变量才会触发收集) 在立即执行触发的 get
里,可以初始化出回调 map
的数据结构 但是回调函数在 effect
里,此时没有 originObj
和 key
,即使已经初始化好回调 map
数据结构,也无法往对应的地方存储回调函数
所以外加一个临时变量存储立即执行触发的 get
对应的回调 map
数据结构中的回调列表,这样在 effect
里就可以往临时变量里存回调了 而在 set
的时候根据 key
获取回调 map
中对应的回调执行即可
(function () {
let dummy;
const callbackMap = new Map();
let tempCallbackList = []; // 临时变量 立即执行的回调之后获取的对应的回调列表
function reactive(data) {
return new Proxy(data, {
get(originData, key) {
console.log("触发get", key);
if (!callbackMap.get(originData)) {
callbackMap.set(originData, new Map());
}
if (!callbackMap.get(originData).get(key)) {
callbackMap.get(originData).set(key, []);
}
tempCallbackList.push(callbackMap.get(originData).get(key));
return originData[key];
},
set(originData, key, newVal) {
console.log("触发set", key);
originData[key] = newVal;
callbackMap
.get(originData)
.get(key)
.forEach((fn) => fn());
return newVal;
},
});
}
const myData = { num: 1 }; // 原数据
const proxyData = reactive(myData); // 代理后数据
function effect(callback) {
tempCallbackList = [];
callback(); // 1. 立即执行一次
tempCallbackList.forEach((list) => {
list.push(callback);
});
}
effect(() => (dummy = proxyData.num)); // 触发立即执行回调中赋值的get
console.log(dummy); // 1 不触发proxy的get set
proxyData.num = 2; // 触发set 和回调中的赋值的get
console.log(dummy); // 2 不触发proxy的get set
/*effect(()=>{
console.log('触发num的回调');
dummy = proxyData.num
}); // 触发立即执行回调中赋值的get
effect( ()=>{
console.log('触发num2的回调')
dummy2 = proxyData.num2
} ); // 触发立即执行回调中赋值的get
console.log(dummy); // 1 不触发proxy的get set
proxyData.num = 2; // 触发set 和回调中的赋值的get
proxyData.num2 = 2; // 触发set 和回调中的赋值的get
console.log(dummy); // 2 不触发proxy的get set*/
})();
👆 在我们测试使用多个 effect
时没有问题,每个 effect
都会往对应的 key 里存储回调 但是当一个 effect
里依赖多个变量时发现了问题,只把回调存储到了最后一个变量的回调里 这是因为临时变量只对应一个 key
的回调列表 所以临时变量应该对应多个 key
的回调函数列表 在存储回调的时候遍历临时变量列表来存储
到这里,已经可以有 vue3
双向绑定数据的雏形 也可以发现我们是通过 get
来做回调存储的 当不触发 get
的时候就不会有回调 如:使用判断语句,只有在运行时才能知道会不会进 get
,在 effect
阶段不知道
4. 深度proxy
另外,proxy
并不自动支持深度代理, key
对应的回调也不没有深度数据
要想实现深度数据的 key,首先想到的可能是递归,但是这里有个更取巧的方法 就是, effect
立即执行阶段,如果是深度数据,只会触发外层的 proxy
我们可以利用判断当前值是对象时返回一个 proxy
代理,让深度的值继续触发 get,形成递归的样子
(function () {
let dummy;
const callbackMap = new Map();
let tempCallbackList = []; // 临时变量 立即执行的回调之后获取的对应的回调列表
function reactive(data) {
return new Proxy(data, {
get(originData, key) {
console.log("触发get", key);
if (!callbackMap.get(originData)) {
callbackMap.set(originData, new Map());
}
if (!callbackMap.get(originData).get(key)) {
callbackMap.get(originData).set(key, []);
}
tempCallbackList.push(callbackMap.get(originData).get(key));
// 判断当前值是对象时继续代理,形成递归
if (typeof originData[key] === "object") {
return reactive(originData[key]);
}
return originData[key];
},
set(originData, key, newVal) {
console.log("触发set", key);
originData[key] = newVal;
callbackMap
.get(originData)
.get(key)
.forEach((fn) => fn());
return newVal;
},
});
}
const myData = { a: { b: 1 } }; // 原数据
const proxyData = reactive(myData); // 代理后数据
effect(() => {
dummy = proxyData.a.b;
});
console.log(dummy);
proxyData.a.b = 2;
console.log(dummy);
})();
这样就让深度的对象类型数据也走一次 proxy
但是这样的写法有点性能问题,每次深度取值都要重新走一次 proxy
即使同一个值已经走过一次
5. 优化深度代理
这里我们用一个 map
存储深度对象的 proxy
,就以对象为 key
其实也像是把深度对象扁平化出来存储了,仅仅为了省去每次深度取值时的重复 proxy
const deepProxyMap = new Map(); // 其实也像是把深度对象扁平化出来存储了,仅仅为了省去每次深度取值时的重复
function reactive(data) {
if (deepProxyMap.has(data)) {
return deepProxyMap.get(data);
}
const proxy = new Proxy(data, {
get(originData, key) {
console.log("触发get", key);
if (!callbackMap.get(originData)) {
callbackMap.set(originData, new Map());
}
if (!callbackMap.get(originData).get(key)) {
callbackMap.get(originData).set(key, []);
}
tempCallbackList.push(callbackMap.get(originData).get(key));
if (typeof originData[key] === "object") {
return reactive(originData[key]);
}
return originData[key];
},
});
deepProxyMap.set(data);
return proxy;
}
结合dom的reactive案例
1. input双向数据绑定(v-model)
2. 拖拽取色值
目前很流行的一种大型项目管理方式 Monorepo