Proxy与Reflect

377 阅读9分钟

前言

Proxy 名为代理,相当于是被代理对象的替代品、中间人,在很多应用开发中都存在这个概念,但大多都存在于三方组件封装,应用都是手动封装一个代理功能,而这个 Proxy 有些不一样,其代替的 Object.defineProperty 等方法,更像是一个为 hook 而生的黑魔法,大多需要配合反射 Reflect一起使用

Reflect 名为反射,这个更是比较常见了,在其他语言语言中也比较常见,例如: runtime运行时等。其实际上就是给我们一个从外部操作对象的接口,其能协助我们在一些领域作出更好的方案(毕竟其他语言基本都有类似的功能,也做出了很多很便利优秀的作品)

ProxyReflect 他们两个在大多数功能中基本都是成对出现,有点黑魔法的功能离不开黑魔法本身意思,有使用它们作出非常优秀的响应式的案例,例如:vue,相信都有所耳闻

本文主要讲解 Proxy、Reflect 的使用,以及他们简单的联合案例,并附上一个简单的响应式的测试案例协助理解

参考地址 ProxyReflect

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.fooproxy['foo']
  • set(target, propKey, value, receiver) :拦截对象属性的设置,比如proxy.foo = vproxy['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.getOwnPropertyNamesObject.getOwnPropertySymbols之和
  • Reflect.isExtensible(target): Object.isExtensible
  • Reflect.preventExtensions(target):Object.preventExtensions
  • Reflect.apply(target, thisArg, args):Function.prototype.apply.call call方法不多说
  • 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中的属性名

怎样实现呢,怎么实现效率更高呢?

最后

就介绍到这里了,学习是进步的源泉