当前位置: 首页 > article >正文

Proxy vs DefineProperty

几年前校招面试的时候被问过一个问题,Vue3/Vue2 如何实现数据和UI的同步,其区别是什么,Vue3的方式优势是什么?

当时背了八股,默写了一通不知所云的代码,面试没过,再也没写过Vue。

今天拿出点时间来写一下这个问题的前因后果,也算是做个笔记吧。

关键点

当我们修改某个属性值后,其"绑定"的UI不会自动更新,通常需要我们手动的再次设置UI的内容

domInstance.textContent = 'newValue'

这就导致,我们每次修改值的时候 都需要手动更新UI,Vue2/3就是简化这个过程。

这个问题的关键点是,如何能够监听到对象属性值的修改,并且在修改的过程中插入一些我们需要的逻辑(如更新UI等)

换句话说,需要将修改属性的过程 变成一个函数

const obj = { a: 100 }

obj.a = 200 // 需要在修改a为200时 监测到这个变化

ES6以前,通常使用DefineProperty的方式来设置get set描述符来实现,这也就是Vue2的方式

DefineProperty

我们通常使用以下的方式设置描述符

defineProperty(设置的对象,设置的属性值,描述符对象)

其中,属性描述符中包含 get set方法,在我们访问某个属性或者给某个属性赋值的时候,会自动调用设置的get set方法,从而实现监听修改和读取,看下面例子

// 被监听的对象      
const obj = {
            _a: 100,
            _b: 200,
            _c: {
                c1: 100,
                c2: 200
            },
        }

// 监听obj对象的属性a的改变
 Object.defineProperty(obj, 'a', {
            get() {
                console.log('obj的a属性被访问')
                return obj._a
            },

            set(newValue) {
                console.log('obj的a属性被修改 新的值为' + newValue)
                obj._a = newValue
            }
        })

使用Object.defineProperty给obj的属性a设置get set监听,当访问属性a 或者读取属性a的时候,就会调用get set方法,相当于"覆盖"了 读取和修改属性a的逻辑

既然对a的修改变成了一个函数,我们就可以在其中加入修改UI的逻辑,实现修改a的过程中, 自动更新其绑定的DOM元素, 如下所示

        Object.defineProperty(obj, 'a', {
            set(newValue) {
                console.log('obj的a属性被修改 新的值为' + newValue)
                domInstance.textContext = newValue // 修改UI内容
                obj._a = newValue
            }
        })

现在,你就实现了Vue2 数据UI同步更新的原理。但是我们还需要添加一些细节

你可能会发现,defineProperty的数据监听,是相对于属性的,这也是其和Proxy 监听的最大区别,如果你想对多个属性进行监听,实现多组数据和UI的同步绑定,你就需要为每一个属性都设置get set 如下:

        const obj = {
            _a: 100,
            _b: 200,
            _c: {
                c1: 100,
                c2: 200
            },
        }

        Object.defineProperty(obj, 'a', {
            get() {
                console.log('obj的a属性被访问')
                return obj._a
            },

            set(newValue) {
                console.log('obj的a属性被修改 新的值为' + newValue)
                obj._a = newValue
            }
        })

        Object.defineProperty(obj, 'b', {
            get() {
                console.log('obj的b属性被访问')
                return obj._b
            },

            set(newValue) {
                console.log('obj的b属性被修改 新的值为' + newValue)
                obj._b = newValue
            }
        })

        Object.defineProperty(obj, 'c', {
            get() {
                console.log('obj的c属性被访问')
                return obj._c
            },

            set(newValue) {
                console.log('obj的c属性被修改 新的值为' + newValue)
                obj._c = newValue
            }
        })

这样写会带来一些问题,比如代码量的增多,大量的definePropery逻辑导致代码不够优雅,同时,对于属性值为引用的情况,需要递归的给每个属性创建get set ,Vue中实现了一个observe函数,专门用来递归的为对象添加get set函数 以实现对每个属性的拦截,大致实现如下

function observe(originObject) {
            Object.keys(originObject).forEach(originKey => {
                // 逐个属性添加代理
                Object.defineProperty(originObject, originKey, {
                    get() {
                        const currentValue = Reflect.get(originObject, '_' + originKey)
                        if (typeof currentValue === 'object' && currentValue !== null) {
                            observe(currentValue)
                        }
                        return currentValue
                    },

                    set(newValue) {
                        // 修改UI
                        domInstance.textContext = newValue
                        Reflect.set(originObject, '_' + originKey, newValue)
                    }
                })
            })

            return originObject
        }

这样为每个属性,都添加getset的方式 无疑带来了更多的空间浪费,除此以外更严重的问题是

1. 当observe函数执行之后,你无法再为新增的属性设置监听,这也是为什么Vue2中,对于数据监听的设置需要放在created钩子之前。

2. 属性描述符只提供了 get set的方式,对于其他的属性操作,比如删除属性等 无法做到监听,这也是为什么Vue中无法监听到属性的删除

Proxy的优势

ES6中引入Proxy,能很好的解决上面的问题,这也是Vue3实现数据UI同步的方式

Proxy是个构造函数,其使用方式是

const proxyObject = new Proxy(被监听的对象,handler)

其中 handler是一个对象,其内部可以传入对多种属性操作的监听函数,如下:

        const proxyObj = new Proxy(obj, {
            // 读取属性
            get: (target, propName) => {
                console.log("obj对象的" + propName + "被读取");
                return target[propName];
            },
            // 设置属性
            set: (target, propName, newValue) => {
                console.log("obj对象的" + propName + "被修改 新的值为:" + newValue);
                target[propName] = newValue;
            },
            // 删除属性
            deleteProperty(target, propName) {
                console.log("obj对象的" + propName + "属性 被删除!");
                Reflect.deleteProperty(target, propName);
            },
            // .. 其他属性监听
        });

我们可以在handler中 实现 get set deletProperty ... 等等的监听函数 来实现代理

Proxy handler实现的方法我们以get set方法举例,

get方法接受参数为:

target: 要访问的对象,这里也就是obj

propName: 要访问的属性名称

receiver?: 实际调用者 后面会说

set方法接受参数为

target: 要修改的对象,这里也就是obj

propName: 要修改的属性名称

newValue: 要修改的新值

receiver?: 实际调用者 后面会说

通过这种方式,我们就可以在修改访问属性的过程中,完成监听,修改DOM,如下:

new Proxy(obj, {
    get: (target, propName,receiver) => {
                console.log("obj对象的" + propName + "被读取");
                return target[propName];
            },
    set: (target, propName, newValue,receiver) => {
                console.log("obj对象的" + propName + "被修改 新的值为:" + newValue);
                target[propName] = newValue;
            },
 });

可以发现,Proxy的监听是以对象为单位的,也就是说,在给对象设置Proxy监听之后,对对象任意属性的操作都可以被监听到,不论属性是否新增,删除,都不受影响。

在Vue3中,同样实现了observe函数,大致实现如下:

        function observe(originObject) {
            return new Proxy(originObject, {
                get(target, propName, receiver) {
                    const currentValue = Reflect.get(target, propName, receiver)
                    if (typeof currentValue === 'object' && currentValue !== null) {
                        // 访问到get的时候 创建新的proxy 后续再访问别的属性 就能检测到了 (lazy) 区别于DefineProperty
                        return observe(currentValue)
                    }
                    return currentValue
                },

                set(target, propName, newValue, receiver) {
                    // 修改UI
                    domInstance.textContext = newValue   
                    return Reflect.set(target, propName, newValue, receiver)
                },
            })
        }

你也许会说,啊 你用Proxy不也得递归处理引用属性吗? 是的,但是其好处在于,

首先,每个引用属性我只需要创建一个Proxy,不需要为每个属性创建get set

其次,创建Proxy的这个过程是在访问(调用get)时 创建的,类似于懒加载的形式, 可以减少开销

比如:

const obj = {
    a: {
        innerA: 100
    }
}

const proxyObj = observe(obj)

当访问obj.a.innerA时,会从左到右依次执行LHS

访问obj.a 调用get 此时由于a是引用,创建a引用的监听,并且返回

此时就变成了 proxyAttributeA.innerA 就会进入刚创建的a的Proxy的get方法,获取innerA的值

这也是为什么Proxy效率更高,同时可以动态的实现对属性的监听。

Reflect & receiver

上面遗留了一个问题 get set的最后一个函数 receiver属性是什么?

ES6中引入了Refelct对象,这个对象可以在宿主环境中直接使用,提供了一些数据拦截的方法,比如:

Reflect.get(target, propertyKey[, receiver]):获取对象的属性值。
Reflect.set(target, propertyKey, value[, receiver]):设置对象的属性值。
Reflect.has(target, propertyKey):判断对象是否有某个属性。
Reflect.deleteProperty(target, propertyKey):删除对象的某个属性。
Reflect.defineProperty(target, propertyKey, attributes):定义对象的新属性。
Reflect.getPrototypeOf(target):获取对象的原型。
Reflect.setPrototypeOf(target, prototype):设置对象的原型。
Reflect.apply(target, thisArgument, argumentsList):调用对象的方法。
Reflect.construct(target, argumentsList[, newTarget]):使用构造函数创建对象。

这些方法中的部分和Object.xxx 方法功能重合,使用Reflect对象的方法可以使对象操作更加规范化和简单,提高代码的可读性和可维护性。

在Proxy中,推荐使用Reflect.get(target,propName) , Reflect.set(tagret,propname,newValue)的方式来修改原始对象的值,但是为什么这么做呢?

正常情况下,用target[propName] 和 Refect.get(target,PropName) 没区别,但是有一些特殊情况会存在差异,比如 原始对象的某个属性是通过 get set  这样的属性描述设置和返回的 就会出现以下问题

const person = {
  _name: "bill",
  get name() {
    return this._name;
  },
};

const personProxy = new Proxy(person, {
  get: (target, propName) => {
    console.log("读取person的" + propName + "属性");
    return target[propName];
  },
});

console.log(personProxy.name); // bill 这个没问题

const student = {
  id: "UWHDIUWOA(J@*HIDJWLOD",
  _name: "Jack",
};

// 把student设置为personProxy的继承对象
Object.setPrototypeOf(student, personProxy);

console.log(student.name); 
// 这里还是bill 因为在Proxy.get中 使用target[propName] 会丢失this 导致this指向person
// 这里需要使用 Reflect.get(,,receiver)

 student的原型__proto__ 指向personProxy,在读取student.name时,会顺着原型链找到personProxy.name 由于name是个get访问器,会返回this._name的值,但是我们期望返回的是student中的_name 也就是期望get中的this应该指向 student 但是由于我们在proxy的get中,使用了target[propName]的方式,把this指向了target 即person。

如何解决这个问题,此时就用到了Reflect

你会说如果把target[propName] 换成Reflect.get(target,propName) 会有效果吗?

对不起没有 我们还需要用到的 是get的最后一个 receiver参数

很多文章把receiver解释成Proxy 其实不全对,receiver指向get的真实调用者

当访问personProxy.name 时,此时的访问者为personProxy对象,此时receiver = 当前Proxy对象

当访问student.name时,此时指向的真正调用者 student对象

拿到了receiver之后,我们需要透传给Reflect.get/set对象,这也是使用Reflect的优势所在,其最后一个参数可以接受一个receiver,如果原始对象的属性为访问器,会将其中的this,赋为receiver,这样在多个对象继承的时候,可以正确的实现多态,如下:

  const reflectPersonProxy = new Proxy(person, {
            get: (target, propName, receiver) => {
                // 这里的reciver 指向真正的属性访问者
                // 使用reflectPersonProxy.name时 receiver指向person对象
                // 使用reflectPersonProxy的parent对象.name 时 receiver指向 parent对象
                console.log("读取person的" + propName + "属性");
                // 把receiver透传给Refelct.get 其会修改get访问器中this的指向
                return Reflect.get(target, propName, receiver);
            },
        });

        const student2 = {
            id: "WHDIUWHDOIAJODJIWWD",
            _name: "Jack",
        };

        Object.setPrototypeOf(student2, reflectPersonProxy);

        console.log(student2.name); // 正确输出 Jack

我们可以查看官方对receiver的描述:

 

同时,只有Reflect.get Reflect.set方法可以接受receiver 这也就对应了 receiver是为了解决 原始对象 get set访问器this指向错误的问题! 

所以,在使用Proxy实现代理时,建议使用Reflect对象完成对原始对象的操作,并且一定要传入receiver! 

 本文所有代码可以在 https://github.com/Gravity2333/learn_proxy 找到


http://www.kler.cn/a/540798.html

相关文章:

  • C语言学习笔记:子函数的调用实现各个位的累加和
  • 12c及以后 ADG主备切换
  • 工业相机在工业生产制造过程中的视觉检测技术应用
  • 问题大集04-浏览器阻止从 本地 发起的跨域请求,因为服务器的响应头 Access-Control-Allow-Origin 设置为通配符 *
  • sklearn基础教程
  • 只需两步,使用ollama即可在本地部署DeepSeek等常见的AI大模型
  • 车载工具报错分析:CANoe、CANalyzer问题:Stuff Error
  • Java 大视界 -- Java 大数据在智能家居中的应用与场景构建(79)
  • Vue:Table合并行于列
  • 用Go实现 SSE 实时推送消息(消息通知)——思悟项目技术4
  • 绘制中国平安股价的交互式 K 线图
  • 31、spark-on-kubernetes中任务报错No space left on device
  • Fastadmin根据链接参数显示不同列表格
  • 10 FastAPI 的自动文档
  • OpenAI 实战进阶教程 - 第十二节 : 多模态任务开发(文本、图像、音频)
  • 持续集成-笔记
  • DeepSeek之于心理学的一点思考
  • Java中有100万个对象,用list map泛型存储和用list对象泛型存储,那个占用空间大,为什么...
  • python两段多线程的例子
  • 网络安全架构分层 网络安全组织架构
  • 什么是蒸馏大型语言模型
  • WiFi配网流程—SmartConfig 配网流程
  • 基于uniapp vue3 的滑动抢单组件
  • Markdown+Vscode+Mindmaster打造读书笔记
  • C# Mutex 锁 使用详解
  • 爬虫案例-爬取某度文档利用飞桨ch_pp-ocrv3模型提高对图片的识别