前言
Proxy 名为代理,相当于是被代理对象的替代品、中间人,在很多应用开发中都存在这个概念,但大多都存在于三方组件封装,应用都是手动封装一个代理功能,而这个 Proxy 有些不一样,其代替的 Object.defineProperty 等方法,更像是一个为 hook 而生的黑魔法,大多需要配合反射 Reflect一起使用
Reflect 名为反射,这个更是比较常见了,在其他语言语言中也比较常见,例如: runtime运行时等。其实际上就是给我们一个从外部操作对象的接口,其能协助我们在一些领域作出更好的方案(毕竟其他语言基本都有类似的功能,也做出了很多很便利优秀的作品)
Proxy、Reflect 他们两个在大多数功能中基本都是成对出现,有点黑魔法的功能离不开黑魔法本身意思,有使用它们作出非常优秀的响应式的案例,例如:vue,相信都有所耳闻
本文主要讲解 Proxy、Reflect 的使用,以及他们简单的联合案例,并附上一个简单的响应式的测试案例协助理解
Proxy
Proxy 在一个对象外部架上一层拦截,这就是代理,我们可以利用它做一些黑魔法(当然离不开黑魔法本身 Reflect)
//算是代替 Object.defineProperty 的好工具了,如下所示
//相当于直接代理了整个对象,里面的所有属性操作都可以统一拦截
var obj = new Proxy({}, {
//target 表示操作的目标对象,propKey 属性名, receiver接受者也就是 proxy
//这些参数都是可以无缝衔接 Reflect 的,可以说用心了,也毕竟是一起出来的
get: function (target, propKey, receiver) {
console.log(`getting ${propKey}!`);
return Reflect.get(target, propKey, receiver);
},
set: function (target, propKey, value, receiver) {
console.log(`setting ${propKey}!`);
return Reflect.set(target, propKey, value, receiver);
}
});
之前的 Object.defineProperty 长这样
//可以重新定义对象的某一个key对应的属性名,然后重新定义
Object.defineProperty(targetObj, key, {
get() {
return that.val;
},
set(newVal) {
that.notify(newVal);
},
writable: true, //可重写,也就是可修改
enumerable: true, //可枚举, 也就是可以通过for in 遍历
configurable: true, //可配置,也就是可以delete删除
});
Proxy的特点
细心有些经验的人,一眼就可以看出来,明显 proxy 更好呀,为什么呢
Object.defineProperty重新定义对象的某一个属性,直接更改了某个对象的某个属性,很方便,但有时需要做一些重复性操作,并且重新定义意味着直接侵入了该对象,属于直接更改对象的内部操作Proxy名为代理,在对象的外部架了一层拦截(中间人),不会直接侵入内部对象,只会通过中间者的协调读写,间接影响对象,相对来说,侵入性比较差,对于开发者来说也更友好一些,并且其伴随者Reflect出现,还能使用很多黑魔法代替 Object 的基础方法- 并且作为代理,如果再重新使用另外一个代理,则同一个对象可以获取不同的表现,把原始对象当做一袋面,有通过 Proxy,有人将面做成了馒头,有人做成了大饼,有人做成了饺子皮,有人做成了疙瘩汤,有人做成面条,不同的 Proxy 将原始对象加工成了更多的意义,也就是 proxy 作为了
中间协调者,相比较 defineProperty,更像一个工厂了
ps:有人说,不用 Proxy 也能加工呀,确实是,但这是 proxy 他本身代表的行为,其他加工不少也是在模仿这个行为,也就是协调者,代理者身份
ps2:Proxy 并不是说不可代替的,我们可以通过面向对象、面向过程等各种手段解决类似的复杂问题,但是当有非常符合场景很好用的东西出现的时候,直接用不好么,成本本身也是一个不小的问题是吧
ps3:既然是代理也需要反射,缺点肯定是性能比直接操作要差一点点,对于功能特性而言,小小的性能牺牲是值得的,并且使用的 Proxy 的地方,大多不是不需要同时连续处理上百万上亿的数据,一般来说问题不大
Proxy 支持拦截的方法
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver) :拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver) :拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey) :拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey) :拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target) :拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey) :拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 - defineProperty(target, propKey, propDesc) :拦截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 - preventExtensions(target) :拦截
Object.preventExtensions(proxy),返回一个布尔值。 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy),返回一个对象。 - isExtensible(target) :拦截
Object.isExtensible(proxy),返回一个布尔值。 - setPrototypeOf(target, proto) :拦截
Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 - construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)。
Proxy可使用地点
前面也简单介绍到了,Proxy 作为一个代理,可以很好的监听更改一些东西,因此对于观察、响应式很友好,后面可以举个案例
另外 proxy 基础就先介绍到这里了,里面东西实际也不多,更多的是怎么使用,基本都是看到会用就 ok了
Reflect
其名字为反射,就像对象的镜子,我们可以通过反射我们可以查看操作对象,属于 js 的黑魔法,利用它我们可以完成很多优秀的操作
前面讲了 在使用 Proxy 的时候,通常都要使用它,那是因为 Proxy 毕竟只是一个代理,没办法直接更改对象,Reflect 配合他才算是完成了 Proxy 的功能,可以说 Relfect 是 Proxy 的基石,相比较 Proxy,Reflect实际是一个更基础的功能,平时也用的更多
并且 Object 的很多方法, Reflect 基本上都有定义,可以说就像镜子一样,反射了对象的所有内容
实际上 Reflect 的本质就是对象基本操作的一个封装库
Reflect对象一共有 13 个静态方法,不多说基本上都是对应着对象的(看看前面的 proxy 更熟悉了)
- Reflect.construct(target, args):构造方法相当于 new
- Reflect.get(target, name, receiver):反射基础 get
- Reflect.set(target, name, value, receiver): 反射基础 set
- Reflect.defineProperty(target, name, desc):
Object.defineProperty - Reflect.deleteProperty(target, name):
delete obj[propKey] - Reflect.has(target, name):
key in Obj - Reflect.ownKeys(target):
Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNames与Object.getOwnPropertySymbols之和 - Reflect.isExtensible(target):
Object.isExtensible - Reflect.preventExtensions(target):
Object.preventExtensions - Reflect.apply(target, thisArg, args):
Function.prototype.apply.callcall方法不多说 - Reflect.getOwnPropertyDescriptor(target, name):
Object.getOwnPropertyDescriptor获取属性描述符,是否可重写、枚举遍历、 - Reflect.getPrototypeOf(target):
Object.getPrototypeOf - Reflect.setPrototypeOf(target, prototype):
Object.setPrototypeOf
ps:需要注意的是 receiver 代表的是绑定对象,里面使用到 this 的话,是指向 receiver,如果没有它的话,那么 proxy 中 this 指向问题则会出现问题哈,这也是很多新手容易忽略的this指向问题,不可忽略,而target就是对象本身不多说了
ps:object还有一些比较常用的,不多介绍,例如: Object.freeze(obj)冻结对象(可冻结原型)保持如初,结构和内容无法修改,Object.seal(obj)密封对象,无法改变结构,但是可以修改内容
由于 Reflect 都是基础操作,感觉没有太多介绍的,用的最多的反而是 get、set
Proxy 与 Reflect
前面抢了 Proxy 和 Reflect 怎么看都非常简单,但是他们结合起来的的小功能却很关键(还需要搭配其他知识点),甚至能够帮我们解决很多痛点,我们就做一个简易的 响应式 + render 把
做一个简单的响应式渲染
当我们给对象赋值时,自动触发渲染函数,这就算一个简单的响应式了(表现就是更改一个属性,页面自动刷新了)
class TestRender {
constructor(obj) {
let that = this
this.objProxy = new Proxy(obj, {
set: function (target, name, value, receiver) {
that.render()
return Reflect.set(target, name, value, receiver);
},
});
this.flag = 0
}
render() {
console.log('我被重新渲染了')
}
}
const rendObj = new TestRender({
name: '啦啦',
age: 20
})
rendObj.objProxy.name = '哈哈'
rendObj.objProxy.name = '啦啦'
rendObj.objProxy.age = 18
rendObj.objProxy.age = 20
//打印结果
我被重新渲染了
我被重新渲染了
我被重新渲染了
我被重新渲染了
优化渲染时机减少次数
上面执行了之后,发现 render 循环调用,我们可以在优化,减少 render 次数,可以通过宏队列与微队列的先后优先级顺序,进行优化渲染回调
class TestRender {
constructor(obj) {
let that = this
this.objProxy = new Proxy(obj, {
set: function (target, name, value, receiver) {
that.preRender()
return Reflect.set(target, name, value, receiver);
},
});
this.flag = 0
}
preRender() {
if (this.flag) {
return
}
this.flag = 1
setTimeout(() => {
this.render()
}, 0);
}
render() {
console.log('我被重新渲染了')
}
}
const rendObj = new TestRender({
name: '啦啦',
age: 20
})
rendObj.objProxy.name = '哈哈'
rendObj.objProxy.name = '啦啦'
rendObj.objProxy.age = 18
rendObj.objProxy.age = 20
//打印结果
我被重新渲染了
课后锻炼
这就完成了优化,看实际上也不麻烦是吧 🤣 看着案例很简单,但是实际 vue 和 react 都不这样,这样怎么用呀是吧,那么直接写一个对象,对象提供一个方法
- 1.将数据传递给该对象(假设数据只有一层),当数据内的属性发生改变时,我们重新执行 render 方法
- 2.在修改数据时,数据实际未发生变化不触发渲染
- 3.render时标记用到的属性(可直接根据修改的属性名,与标记渲染名字,渲染时打印需要渲染的key即可,算是模拟只渲染某个节点),当触发某个render时,只更新打印指定render中的属性名
怎样实现呢,怎么实现效率更高呢?
最后
就介绍到这里了,学习是进步的源泉