代理模式
HeadFirst设计模式中,对代理模式的介绍如下。其主要的思想是将访问数据对象这一过程解耦。
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
使用方式
Proxy 是 ES6 中新增的功能,可以用来自定义对象中的操作,设计思想主要是基于设计模式中的代理模式,其原理图大致如下。
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
每次获取proxy,这个proxy就会返回对data的深拷贝,而要对data中的属性进行增删查等操作,也是直接对proxy下手就行。通过对整个对象的代理,就避免了访问之前要明确data中有什么属性的这一个过程。这样就实现了一个解耦合的过程,避免了直接操作data对象。基本的使用示例如下。
let p = new Proxy(target, handler);
// `target` 代表需要添加代理的对象
// `handler` 用来自定义对象中的操作
let person01 = {
name: "",
testObj: {
key1: "1",
key2: "2",
key3: "3",
}
};
// 设置了一个person01的代理
let personProxy01 = new Proxy(person01, {
get(target,key){
// console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
return target[key];
},
set(target,key,value){
// console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
target[key]=value;
}
})
proxy中的handler主要存放的是一些代理钩子函数,通过这些函数我们可以自定义一些数据劫持的方式,以此可以实现一个更优秀的订阅发布模式。
在对象的操作方法里面,建议使用ES6新增的Reflect,而Reflect里定义了对象操作的方法,利用Reflect可以方便地对被代理对象的操作。
handler中主要定义的函数如下所示(es2015中的源码)。
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
代理陷阱 | 覆写的特性 | 默认特性 |
---|---|---|
get | 读写一个属性值 | Reflect.get() |
set | 写入一个属性 | Reflect.set() |
has | in操作 | Reflect.has() |
deleteProperty | delete操作符 | Reflect.deleteProperty() |
getAPrototypeof | Object.getAPrototypeof () | Reflect.getAPrototypeof () |
setAPrototypeof | Object.setAPrototypeof () | Reflect.setAPrototypeof () |
isExtensible | Object.isExtensible() | Reflect.isExtensible() |
preventExtensions | Object.preventExtensions() | Reflect.preventExtensions() |
getOwnPropertyDescriptor | Object.getOwnPropertyDescriptor() | Reflect.getOwnPropertyDescriptor() |
defineaProperty | Object.defineaProperty() | Reflect.defineaProperty() |
ownKeys | Object.keys() 、 Object.getOwnPropertyNames()和 Object.getOwnPropertySysmbols() | Reflect.ownKeys() |
apply | 调用一个函数 | Reflect.apply() |
construct | 用new调用一个函数 | Reflect.construct() |
其中,最常使用的就是set和get了。
set(trapTarget,key,value,receiver)
这个set陷阱默认接收 四个参数
-
trapTarget 用于接收属性(代理的目标)的对象 -
key 要写入的属性键(字符串或Symbol类型) -
value 被写入的属性的值 -
receiver 操作发生的对象(通常是代理)
get(trapTarget,key,receiver)
默认接受三个参数
-
trapTarget 被读取属性源对象(代理的目标) -
key 要读取的属性键(字符串或Symbol类型) -
receiver 操作发生的对象(通常是代理)
自己实现一个Proxy
思路
-
参数为(target, handle),接受target目标数据对象,handle则是写代理陷阱函数
-
定义一个深拷贝函数,实现对target的深拷贝(这里的深拷贝要支持对象、函数、数组等)
-
获取target的深拷贝targetCopy
-
遍历targetCopy.keys(),利用Object.defineProperty来为每个key覆写函数,并放入handler中对应函数
-
设置一个拦截器,对后来添加入targetCopy的属性,为这个key添加覆写函数,并放入handler中对应函数(暂时无思路)
-
返回这个targetCopy
按照思路写出来,大致如下。
function MyProxy(target, handle) {
this.clone = function (myObj) {
// 是否有嵌套结构
if (typeof (myObj) !== 'object' || myObj == null) {
return myObj;
}
var newObj = new Object();
// 递归
for (let i in myObj) {
newObj[i] = this.clone(myObj[i]);
}
return newObj;
}
//深度克隆目标对象
let targetCopy = this.clone(target);
// 遍历目标克隆对象的所有属性
// 这里有一个问题,就是它只能操作已有的属性
// 如果是后来往proxy添加的属性呢?怎么从proxy传给data本身?
Object.keys(targetCopy).forEach(function (key) {
Object.defineProperty(targetCopy, key, {
get: function () {
//
return handle.get && handle.get(target, key)
},
set: function (newVal) {
if (!handle.set) {
handle.set(target, key, newVal);
} else {
// 对象有这个属性时才允许被操作
handle.set && handle.set(target, key, newVal);
}
}
});
});
// 返回
return targetCopy;
}
下面来实践一下效果如何?
// ---------------------------------定义data--------------------------
let person01 = {
name: "",
testObj: {
key1: "1",
key2: "2",
key3: "3",
},
list: [1,2,3]
};
let person02 = {
name: "",
testObj: {
key1: "1",
key2: "2",
key3: "3",
},
list: [1,2,3]
};
// ----------------------------------设置proxy-------------------------
let personProxy01 = new Proxy(person01, {
get(target, key) {
// console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
return target[key];
},
set(target, key, value) {
// console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
target[key] = value;
// return Reflect.set(target, key, value);
}
})
let personProxy02 = new MyProxy(person02, {
get(target, key) {
// console.log('get方法被拦截。。。 实现原理为通过属性的getter驱动函数调用该方法');
return target[key];
},
set(target, key, value) {
// console.log('set方法被拦截。。。实现原理为通过属性的setter驱动函数调用该方法');
target[key] = value;
}
})
// ---------------------------------------------------------------------------
console.log("-------------------------es6自带的proxy---------------------");
personProxy01.name = 'hzs'; // set方法被拦截。。。
personProxy01.testObj = {
key1: "11",
key2: "22",
key3: "33",
}
personProxy01.list[0] = 5
console.log(personProxy01.name); // get方法被拦截。。。
console.log(personProxy01.testObj);
console.log(personProxy01.testObj.key1);
console.log(personProxy01.list);
console.log("--------------------------------------");
console.log(person01.name); // get方法被拦截。。。
console.log(person01.testObj);
console.log(person01.testObj.key1);
console.log(person01.list);
console.log("-----------------自己尝试实现的proxy---------------------");
personProxy02.name = 'hzs'; // set方法被拦截。。。
personProxy02.testObj = {
key1: "11",
key2: "22",
key3: "33",
}
personProxy02.list[0] = 5
console.log(personProxy02.name); // get方法被拦截。。。
console.log(personProxy02.testObj);
console.log(personProxy02.testObj.key1);
console.log(personProxy02.list);
console.log("--------------------------------");
console.log(person02.name)
console.log(person02.testObj)
console.log(person02.testObj.key1);
console.log(person02.list);
console.log("-----------尝试往data本身添加新属性,proxy能否监听到?----------------")
person01.sex = "M"
person02.sex = "W"
console.log(person01.sex)
console.log(person02.sex)
console.log("-----------尝试向proxy添加新属性,data本身能否获取到?----------------")
personProxy01.home = "黄土高坡"
personProxy02.home = "青青草原"
console.log(person01.home)
console.log(personProxy01.home)
console.log(person02.home)
console.log(personProxy02.home)
console.log("-----------尝试往data本身添加function,proxy能否监听到?----------------")
person01.isMan = function(sex){
return sex === "M";
}
person02.isWomen = function(sex){
return sex === "W"
}
console.log(person01.isMan("M"))
console.log(person02.isWomen("W")) // undefined
console.log("-----------尝试向proxy添加function,data本身能否获取到?----------------")
personProxy01.isWomen = function(sex){
return sex === "W";
}
personProxy02.isMan = function(sex){
return sex === "M"
}
console.log(person01.isWomen("W"))
// console.log(person02.isMan("M")) 会报错
运行结果如下图:
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
可以看到,前面基本上是没什么问题的,但是到后面,当给proxy添加data中没有的值时,自定义的proxy并不能将新添加的属性加入到data中,而es6自带的proxy则可以实现。我也不太知道该怎么改进,因为object.defineProperty并不提供对于对象本身的拦截。
不过,实际上我个人并不提倡通过对proxy向data中添加没有的数据,一般这种情况下还是直接操作dataObj本身就好了,这也比较符合常用的逻辑。proxy实现的主要目的还是帮助我们监听整个对象,在访问数据对象的时候实现解耦,不用过度关心dataObj中有什么属性。
Proxy使用示例:实现数据绑定
众所周知,VUE3之前,双向绑定是使用Object.defineProperty,而VUE3则是基于proxy来实现的。为什么呢?肯定是为了改进一些缺陷啊!
Object.defineProperty 不足
-
无法监听数组的变化:数组的这些方法是无法触发set的:push, pop, shift, unshift,splice, sort, reverse.,vue中能监听是因为对这些方法进行了重写 -
只能监听属性,而不是监听对象本身,需要对对象的每个属性进行遍历。对于原本不在对象中的属性难以监听。
实现
可以很方便的使用 Proxy 来实现一个数据绑定和监听
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let dataObj = { a: 1 }
let value
let p = onWatch(dataObj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
![[es6]proxy的原理和实现 [es6]proxy的原理和实现](https://www.bmabk.com/wp-content/uploads/2022/05/post-loading.gif)
结语
对于代理模式、defineProperty还有使用proxy实现双向绑定这块,我会争取在写一些学习笔记来继续发出的。
参考资料
《js之proxy实现》 https://blog.csdn.net/qq_38603437/article/details/92855683
《前端进阶之道》 https://yuchengkai.cn/docs/frontend/#proxy
《headfirst设计模式》
原文始发于微信公众号(豆子前端):[es6]proxy的原理和实现
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/56818.html