当前位置: 首页 > Web前端 > vue.js

【转载】深入理解Proxy和defineProperty

时间:2023-04-01 11:44:10 vue.js

据悉,Vue3.0正式版将于本月(8月)发布。从发布到正式项目,还有一定的过渡期,但是不能等到正式投入项目才学会Vue3。提前学习,让你掌握Vue3。但是,在学习Vue3之前,你需要了解一下Proxy,Proxy是Vue3.0实现双向数据绑定的基础。理解代理模式的例子作为一个单身的钢铁直男程序员,小王最近逐渐喜欢上了前台小姐姐,但是对前台小姐姐并不熟悉,所以他决定委托给前台小姐姐比较熟悉的前台小姐姐帮自己架起一座桥梁。于是小王请了UI小姐姐吃了一顿大餐,然后拿出一封情书托付给了前台小姐姐。情书说喜欢你,想睡你,不愧是钢铁直男。不过这样写也没用,UI小姐姐嘴巴短,帮着把情书改成了我喜欢你,想和你一起在辰辉的澡下醒来,然后递给了那个女孩前台。虽然婚介是否成功不得而知,但这个故事告诉我们,小王活该单身。其实以上就是代理模式的一个比较典型的例子。小王想给前台姑娘送情书,但因为不熟,便委托了相当于代理人的UI小姐,代小王完成了情书的投递。作者:前端优优玩链接:https://zhuanlan.zhihu.com/p/...来源:知乎版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。扩展上面的例子,我们来思考一下Vue的数据响应原理,比如下面的代码constxiaowang={love:'我喜欢你,我想和你一起睡'}//给姐姐发情书functionsendToMyLove(obj){console.log(obj.love)return'流氓,迷路'}console.log(sendToMyLove(xiaowang))如果没有UI小姐姐而不是发情书,就说明结局悲剧了。想想Vue2.0的双向绑定,双向绑定是通过Object.defineProperty监控的属性get和set方法实现的。这个Object.defineProperty相当于UI小姐姐constxiaowang={loveLetter:'我喜欢你,我想和你一起睡'}//UI小姐姐AgentObject.defineProperty(xiaowang,'love',{get(){returnxiaowang.loveLetter.replace('Sleep','沐浴牵牛花')}})//给小姐姐写一封情书functionsendToMyLove(obj){console.log(obj.love)return'小伙子挺有诗意的,不过我不喜欢,滚出去'}console.log(sendToMyLove(xiaowang))虽然还是个悲剧故事,因为派奔驰可能成功率更高。但是我们可以看到Object.defineproperty可以拦截对象已有的属性,然后再做一些额外的操作。存在的问题在Vue2.0中,双向数据绑定是通过Object.defineProperty监听对象的各个属性,然后在get和set方法中通过发布订阅者方式实现数据响应,但是存在一定的缺陷。例如,它只能监视现有的属性,而不能对属性的添加和删除做任何事情。同时不能监听数组的变化,所以在Vue3.0中被更强大的Proxy取代。理解ProxyProxy是ES6引入的一个新特性,可以用来拦截js的操作方法,从而对这些方法进行代理操作。用Proxy重写上面的例子比如我们可以通过Proxy重写上面的情书情节:target,key){if(key==='loveLetter'){returntarget[key].replace('Sleep','一起在朝霞下醒来')}}})//送给小姐姐LoveLetterfunctionsendToMyLove(obj){console.log(obj.loveLetter)return'小伙子是不是挺有诗意的,老婆不喜欢,滚'}console.log(sendToMyLove(proxy))作者:某某前端播放链接:https://zhuanlan.zhihu.com/p/...来源:知乎版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。看到这样的场景,请使用Object.defineProperty和Proxy来完善下面的代码逻辑。functionobserve(obj,callback){}constobj=observe({name:'子君',sex:'男'},(key,value)=>{console.log(`属性值[${key}]改为[${value}]`)})//这段代码执行后,输出属性[name]的值发生变化For[SisterPaper]obj.name='SisterPaper'//After这段代码执行后,输出属性[sex]的值改为[Female]obj.sex='Female'看完上面的代码,希望你能先自己实现下面的。接下来,我们使用Object.defineProperty和Proxy来实现上面的逻辑。UseObject.defineProperty/***请实现这个函数,让下面的代码逻辑正常运行*@param{*}objobject*@param{*}callback回调函数*/functionobserve(obj,callback){constnewObj={}Object.keys(obj).forEach(key=>{Object.defineProperty(newObj,key,{configurable:true,enumerable:true,get(){returnobj[key]},//当值修改了属性,会调用set,然后set中可以调用回调函数name:'子君',sex:'男'},(key,value)=>{console.log(`property[${key}]修改为[${value}]`)})//这段代码执行后,输出属性[name]的值变为[girl]obj.name='妹纸'//这段代码执行后,输出属性[sex]的值发生变化到[female]obj.name='Female'使用Proxyfunctionobserve(obj,callback){returnnewProxy(obj,{get(target,key){returntarget[key]},set(target,key,value){target[key]=valuecallback(key,value)}})}constobj=observe({name:'子君',sex:'男'},(key,value)=>{console.log(`property[${key的值}]修改为[${value}]`)})//这段代码执行后,输出属性[name]的值修改为[妹纸]obj.name='妹纸'//这段代码执行后,输出属性[sex]的值变为[female]obj.name='female'通过以上两种不同的实现方式,我们可以大致了解Object的用法。defineProperty和Proxy,但是当给对象添加新的属性时,区别comesout,例如//add公众号fieldobj.gzh='somefunatthefrontend'新的属性不能用Object.defineProperty监听,但是可以用Proxy监听比较上面两块代码,我们可以发现以下差异。Object.defineProperty监控对象的每个属性,而Proxy监控对象本身。使用Object.defineProperty需要遍历对象的每一个属性,对性能会有一定的影响。AffectingProxy也可以监听新添加的属性,但是Object.defineProperty不行。认识Proxy的概念和语法在MDN中,Proxy的介绍是这样的:Proxy对象用于定义基本操作(如属性查找、赋值、枚举、函数调用等)的自定义行为。这意味着什么?代理就像一个拦截器。它可以在读取对象的属性、修改对象的属性、获取对象属性列表、通过forin循环等操作时拦截对象上的默认行为,然后自己定制。这些行为,比如上面例子中的set,我们拦截默认的set,然后在自定义的set中添加一个回调函数来调用Proxy。语法格式如下/***target:要兼容的对象,可以是对象,数组,函数等。*handler:是一个对象,包含了可以监控这个对象的行为函数,比如`get`而上面例子中的`set`*会同时返回一个新的对象proxy,为了能够触发handler里面的函数必须使用返回值来进行其他操作,比如修改值*/constproxy=newProxy(target,handler)在上面的例子中,我们已经使用了handler中提供的get和set方法,接下来我们就来一一了解一下handler中的方法。作者:前端优优玩链接:https://zhuanlan.zhihu.com/p/...来源:知乎版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。handler中的方法列表handler中的方法可以有以下十三个方法,每个方法对应一个或多个对proxy代理对象handler的操作行为。get通过proxy读取对象中的属性时,会进入get钩子函数中的handler.set。当使用代理为对象设置和修改属性时,会进入sethook函数中的handler.has。当使用in判断property是否在proxy代理对象中时,会触发has,例如constobj={name:'子君'}console.log('name'inobj)handler.deleteProperty当使用delete删除时对象中的一个属性,会进入deleteProperty钩子函数handler.apply代理监听一个函数,当调用这个函数时,会进入apply钩子函数handle.ownKeys,通过Object获取对象的信息。getOwnPropertyNames、Object.getownPropertySymbols、Object.keys、Reflect.ownKeys,会进入ownKeys钩子函数handler.construct,使用new运算符时,会进入钩子函数handler.defineProperty,使用Object.defineProperty修改属性修饰符时,会进入钩子函数handler.getPrototypeOf读取对象原型时,会进入这个钩子函数handler.setPrototypeOf设置对象原型时,会进入这个钩子函数handler.isExtensible当通过Object.isExtensible判断对象是否可以添加新属性时,进入这个钩子函数handler.preventExtensions当通过Object.preventExtensions来设置对象不能修改新属性时,进入这个钩子函数handler.getOwnPropertyDescriptor到当获取代理对象的某个属性的属性描述时触发该操作,比如执行Object.getOwnPropertyDescriptor(proxy,"foo")会进入这个钩子函数。Proxy提供了十三种拦截对象操作的方法。本文主要选取其中一些在Vue3中比较重要的进行讲解。其余的建议可以直接看MDN关于Proxy的介绍详细介绍get。通过代理读取对象中的属性时,会进入get钩子函数。当我们从代理读取属性时,会触发get钩子函数。get函数的结构如下/***target:目标对象,即proxy代理的对象*key:要访问的属性名*receiver:receiver相当于属性的this我们要看,一般*就是代理对象本身,关于接收者的作用,后面的文章会详细讲解*/handle.get(target,key,receiver)的例子。我们在工作中经常会有封装axios的需求。在封装过程中,我们还需要对请求异常进行封装,比如返回不同的状态码异常信息不同,下面是部分状态码及其提示信息://状态码提示信息consterrorMessage={400:'Badrequest',401:'系统未授权,请重新登录',403:'RefusedAccess',404:'请求失败,未找到资源'}//如何使用constcode=404constmessage=errorMessage[code]console.log(message)但是有个问题,状态码有很多,我们不能把每一个状态码都枚举出来,所以对于一些异常的状态码,我们希望能够统一提示。如果提示是系统异常,请联系管理员。这时候可以使用Proxy代理错误信息//状态码提示信息consterrorMessage={400:'Badrequest',401:'系统未授权,请重新登录',403:'拒绝访问',404:'请求失败,未找到资源'}constproxy=newProxy(errorMessage,{get(target,key){constvalue=target[key]returnvalue||'系统异常,请联系administrator'}})//输出错误请求console.log(proxy[400])//输出系统异常,请联系管理员console.log(proxy[500])设置正确当给对象内部的属性赋值时,会触发set。当给对象中的属性赋值时,会触发set。set函数的结构如下/***target:目标对象,即proxy代理的对象*key:要赋值的属性名*value:要赋给target属性的新值*receiver:和get*/handle.set(target,key,value,receiver)的receiver基本一样.对于这些异常值,需要在录入时进行处理。比如大于100的值转为100,小于0的值转为0。这时候可以使用Proxy集合,赋值时,处理数据constnumbers=[]constproxy=newProxy(numbers,{set(target,key,value){if(value<0){value=0}elseif(value>100){value=100}target[key]=value//对于集合,如果操作成功,必须返回true,否则视为失败returntrue}})proxy.push(1)proxy.push(101)proxy.push(-10)//output[1,100,0]console.log(numbers)vs.Vue2.0使用Vue2.0时,如果给对象添加新的属性,往往需要调用$set,这是因为Object.defineProperty只能监听已有的属性,但是无法监听新的属性,$set相当于手动给对象添加新的属性,然后触发数据响应但是对于Vue3.0,因为使用了Proxy,在其sethook函数中可以监听新的属性,所以不再需要使用$setconstobj={name:'子君'}constproxy=newProxy(obj,{set(target,key,value){if(!target.hasOwnProperty(key)){console.log(`新属性${key},值为${value}`)}target[key]=valuereturntrue}})//添加公众号属性//输出添加了gzh属性,值为前端someplayproxy.gzh='frontendsomeplay'作者:前端someplay链接:https://zhuanlan.zhihu.com/p/...来源:知道版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。has在使用in判断属性是否在代理对象中时,会触发has/***target:目标对象,即代理所代理的对象*key:要判断的key是否在target*/handle.has(target,key)例子一般我们在js中声明私有属性的时候,属性名都会以_开头。对于这些私有属性,不需要外部调用,所以最好能隐藏起来。此时,通过has判断一个属性是否在对象中时,如果以_开头,则返回falseconstobj={publicMethod(){},_privateMethod(){}}constproxy=newProxy(obj,{has(target,key){if(key.startsWith('_')){returnfalse}returnReflect.get(target,key)}})//输出falseconsole.log('_privateMethod'inproxy)//输出trueconsole.log('publicMethod'inproxy)作者:前端的一些玩法链接:https://zhuanlan.zhihu.com/p/...来源:知道版权归作者所有。商业转载请联系作者授权,非商业转载请注明出处。deleteProperty使用delete删除对象中的某个属性时,会进入deleteProperty`interceptor/***target:目标对象,即通过proxy代理的对象*key:要删除的属性*/handle。deleteProperty(target,key)示例现在有一个用户信息对象。部分用户信息只允许查看,不能删除或修改。为此,使用Proxy可以对不能删除或修改的属性进行拦截并抛出异常,如下constuserInfo={name:'子君',gzh:'前端玩的东西',sex:'男',age:22}//只能删除用户名和公众号constreadonlyKeys=['name','gzh']constproxy=newProxy(userInfo,{set(target,key,value){if(readonlyKeys.includes(key)){thrownewError(`Property${key}cannotbemodified`)}target[key]=valuereturntrue},deleteProperty(target,key){if(readonlyKeys.includes(key)){thrownewError(`property${key}cannotbedeleted`)return}deletetarget[key]returntrue}})//errordeleteProxy.name对比Vue2.0其实和$set解决的问题类似。Vue2.0无法监听到属性被删除,所以提供了$delete来删除属性,但是对于Proxy来说,可以监听到删除操作,所以其他操作就不需要用$delete了。在上面,我们提到了Proxyhandler提供了十三个函数。上面我们列出了三个最常用的函数。其实各个的用法基本一样,比如ownKeys,当传入Object.getOwnPropertyNames,Object.getownPropertySymbols,Object.keys,Reflect.ownK当eys获取到对象的信息后,就会进入ownKeys钩子函数。使用它,我们可以保护一些我们不想公开的属性。例如,一般认为_开头的是私有属性,所以当使用Object.keys想要获取对象的所有key时,可以屏蔽所有以_开头的属性。关于剩下的属性,我建议你多看看MDN里面的介绍。反映在上面,我们通过直接操作target来获取属性的值或者修改属性的值,但实际上ES6已经为我们提供了调用Proxy内部对象默认行为的API,即,反映。比如下面的代码constobj={}constproxy=newProxy(obj,{get(target,key,receiver){returnReflect.get(target,key,receiver)}})你可能会看到上面的代码和direct和target[key]的使用方式没有区别,但其实Reflect的出现是为了让对Object的操作更加规范。比如我们要判断某个prop是否在一个对象中,我们通常使用in,即constobj={name:'子君'}console.log('name'inobj)但是上面的操作是命令式语法,通过Reflect可以转化为函数式语法,更加规范Reflect.has(obj,'name')除了has和get,其实Reflect一共提供了十三个静态方法。这十三个静态方法与Proxyhandler上的十三个方法是一一对应的。通过Proxy和ReflectCombined的结合,可以拦截对对象的默认操作。当然,这也属于函数式元编程的范畴。综上所述,可能有同学会有疑惑,不会Proxy和Reflect是不是就不能学Vue3.0了?其实了解这些并不影响学习Vue3.0,但是想要深入了解Vue3.0,了解这些还是很有必要的。比如在使用Vue2的时候经常有人问,为什么我通过索引修改数组的值后界面没有变化?当你了解了Object.defineProperty的用法和局限性后,你就会恍然大悟原来是这么回事。