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

Vue3为什么要用Proxy来重构响应系统

时间:2023-03-31 18:33:14 vue.js

前言先看一下官方对它的定义:用于定义基本操作的自定义行为proxy修改程序的默认形状,相当于编程语言层面它属于元编程(metaprogramming)。或者在运行时做一部分应该在编译时完成的工作一段代码理解元编程#!/bin/bash#metaprogramecho'#!/bin/bash'>programfor((I=1;I<=1024;I++))doecho"echo$I">>programdonechmod+xprogram这个程序每次执行的时候可以帮我们生成一个名为program的文件。文件内容为1024行echo。如果我们手动写1024行代码,效率很明显低效率元编程的优点:与手动编写所有代码相比,程序员可以获得更高的工作效率,或者让程序更灵活地处理新情况而无需重新编译代理翻译成proxy,可以理解为在操作目标对象之前设置一层proxy,把我们应该手动编写的所有程序都交给proxy。生活中也有很多“代理”,比如代购和中介,因为他们所有的动作都不会直接联系以实现目标文本本文是Vue3源码系列的前几篇之一。Proxy的科普文章与Vue3无关。肯定会完全不一样。前置章节包括为什么要学源码)理解Typescript)理解函数式编程理解Proxy,弄清楚Set、Map、WeakSet、WeakMap,然后介绍Proxy的基本使用语法。是任何类型的对象,包括原生数组、函数,甚至是另一个proxyhandler通常有一个函数作为属性来自定义拦截行为的对象constproxy=newProxy(target,handle)例如constorigin={}constobj=newProxy(origin,{get:function(target,propKey,receiver){return'10'}});obj.a//10obj.b//10origin.a//undefinedorigin.b//undefined在上面的代码中,我们为空的get设置了一层代理对象,而所有的get操作都会直接返回我们自定义的数字10。需要注意的是,代理只会对代理对象生效。例如,上面的origin没有效果。Handler对象常用的方法和方法描述了handler.has()in操作符的catcher处理程序。get()用于属性读取操作的捕手。handler.set()属性设置动作的捕手。handler.deleteProperty()删除操作符的捕手。handler.ownKeys()Object.getOwnPropertyNames方法和Object.getOwnPropertySymbols方法的捕手。handler.apply()函数为该操作调用一个捕手。handler.construct()newoperator的捕手下面重点介绍handler.get,其他方法的使用类似,不同的是参数的不同handler.getget我们在上面的例子中已经体验过了,现在介绍一下detail,用于读取代理目标对象的属性,接受三个参数obj=newProxy(person,{get:function(target,propKey){if(propKeyintarget){returntarget[propKey];}else{thrownewReferenceError("Propname\""+propKey+"\"does不存在。”);}}})obj.like//vuejsobj.test//UncaughtReferenceError:Propname"test"doesnotexist.上面代码的意思是在读取代理目标的值时,如果有值就直接返回。如果没有值,则抛出自定义错误。注意:如果要访问的目标属性是不可写不可配置的,则返回值必须与目标属性的值相同。如果要访问的目标属性没有配置访问方法,即如果get方法未定义,则返回值必须未定义,如下例constobj={};Object.defineProperty(obj,"a",{configurable:false,enumerable:false,value:10,writable:false})constp=newProxy(obj,{get:function(target,prop){return20;}})p.a//UncaughtTypeError:'get'在代理上:属性'a'是一个区域d-onlyandnon-configurable..RevocableProxy代理有一个独特的静态方法,Proxy.revocable(target,handler)Proxy.revocable()方法可以用来创建一个可撤销的代理对象。该方法的返回值为一个对象,其结构为:{"proxy":proxy,"revoke":revoke}proxy代表新生成的代理对象本身,与一般方式newProxy创建的代理对象没有区别(target,handler),除了它可以通过revoke方法进行撤销,用它生成的代理对象在调用的时候不用加任何参数就可以撤销。该方法常用于完全关闭对目标对象的访问,如下例所示consttarget={name:'vuejs'}const{proxy,revoke}=Proxy.revocable(target,handler)proxy.name//正常值输出vuejsrevoke()//取值后关闭代理,并撤销代理proxy.name//TypeError:RevokedProxy的应用场景Proxy的应用范围很广,下面列举几个典型的应用场景。validator想要一个数字,取回来是一个字符串,是不是很惊喜?是不是很意外?接下来,我们使用Proxy来实现一个逻辑上分离的数据格式验证器。嗯,很好吃!consttarget={_id:'1024',name:'vuejs'}constvalidators={name(val){returntypeofval==='string';},_id(val){returntypeofval==='number'&&val>1024;}}constcreateValidator=(target,validator)=>{returnnewProxy(target,{_validator:validator,set(target,propkey,value,proxy){letvalidator=this._validator[propkey](value)if(验证器){returnReflect.set(target,propkey,value,proxy)}else{throwError(`无法将${propkey}设置为${value}。类型无效。`)}}})}constproxy=createValidator(target,validators)proxy.name='vue-js.com'//vue-js.comproxy.name=10086//未捕获的错误:无法将名称设置为10086。无效的type.proxy._id=1025//1025proxy._id=22//未捕获的错误:无法设置_idto22.Invalidtypeprivateattribute在日常写代码的过程中,我们要定义一些私有属性,通常在团队中都是约定好的。大家按照约定在变量名前加上下划线_或者其他格式,表示这是一个私有的Attributes,但是我们不能保证他能真正的“私有化”。让我们使用Proxy轻松实现私有属性拦截consttarget={_id:'1024',name:'vuejs'}constproxy=newProxy(target,{get(target,propkey,proxy){if(propkey[0]==='_'){throwError(`${propkey}isrestricted`)}returnReflect.get(target,propkey,proxy)},set(target,propkey,value,proxy){if(propkey[0]==='_'){throwError(`${propkey}isrestricted`)}returnReflect.get(target,propkey,value,proxy)}})proxy.name//vuejsproxy._id//未捕获的错误:_idisrestrictedproxy._id='1025'//UncaughtError:_idisrestrictedProxy使用场景还有很多,就不一一列举了。在action的生命周期中做一些具体的处理,那么Proxy就适合了。为什么要用Proxy来重构?在Proxy之前,JavaScript提供了Object.defineProperty,允许拦截对象的getter/setter。Vue3.0之前的双向绑定是通过defineProperty实现的。在3.0中,它被重构为Proxy。那么两者有什么区别呢?首先,让我们再次回顾一下它的定义。Object.defineProperty()方法将直接在一个对象上定义一个新的属性,或者修改一个对象的现有属性,并返回该对象。上面突出显示的两个词,在对象上,Attribute,我们可以理解为处理对象上某个属性的语法要定义或修改的描述符Object.defineProperty(obj,prop,descriptor)例如constobj={}Object.defineProperty(obj,"a",{value:1,writable:false,//是否可写configurable:false,//是否可配置enumerable:false//是否Enumerable})//上面给出了三个false,下面的相关操作很容易理解obj.a=2//invaliddeleteobj.a//invalidfor(keyinobj){console.log(key)//Invalid}definePropertyinVueVue3之前的双向绑定是通过defineProperty的getter和setter实现的。先来体验getter和setterconstobj={};Object.defineProperty(obj,'a',{set(val){console.log(`开始设置新值:${val}`)},get(){console.log(`Startreadingproperty`)return1;},writable:true})obj.a=2//开始设置新值:2obj.a//开始获取属性看到这里,相信有同学已经想到了双向绑定背后的过程,其实很简单,只要我们观察对象的属性变化,然后通知更新视图。下面摘录一段Vue源码中的核心实现验证。接下来这部分就一笔带写了,不是本文的重点//源码位置:https://github.com/vuejs/vue/blob/ef56??410a2c/src/core/observer/index.js#L135//...Object.defineProperty(obj,key,{enumerable:true,configurable:true,get:functionreactiveGetter(){//...if(Dep.target){//收集依赖项dep.depend()}returnvalue},set:functionreactiveSetter(newVal){//...//通知视图更新dep.notify()}})为什么对象的new属性没有更新这个问题应该95%以上用过vue的同学都遇到过data(){return{obj:{a:1}}}methods:{update(){this.obj.b=2}}上面的伪代码,当我们执行update到updateobj,我们期望view相应的更新是的,其实不好理解。首先要了解vue中datainit的时机。数据初始化是创建生命周期之前的操作。它会给数据绑定一个观察者,然后数据中的字段更新会通知依赖收集器Dep触发视图更新然后我们返回到defineProperty本身,也就是对对象上的属性进行操作,而不是对象本身.不会有getters和setters,这就解释了为什么新添加的视图没有更新。有很多解决方案。Vue提供的全局$set的本质也是对新增属性的手动观察者//源码位置https://github.com/vuejs/vue/blob/dev/src/core/observer/index.js#L201functionset(target:Array|Object,key:any,val:any):any{//....if(!ob){target[key]=valreturnval}defineReactive(ob.value,key,val)ob.dep.notify()returnval}arraymutation由于JavaScript的限制,Vue无法检测到以下数组mutations:当您直接通过索引设置数组项时,例如:vm.items[indexOfItem]=newValue先看一段代码varvm=newVue({data:{items:['1','2','3']}})vm.items[1]='4'//查看不是更新的文档已经说明了,但这不是defineProperty的错,而是裕达在性能上的设计取舍。下面的代码可以验证functiondefineReactive(data,key,val){Object.defineProperty(data,key,{enumerable:true,configurable:true,get:functiondefineGet(){console.log(`getkey:${key}val:${val}`);returnval;},set:functiondefineSet(newVal){console.log(`setkey:${key}val:${newVal}`);val=newVal;}})}functionobserve(data){Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);})}lettest=[1,2,3];观察(测试);测试[0]=4//setkey:0val:4虽然说索引变化不是defineProperty的错,但是defineProperty确实不可能加索引,所以才有了数组变异的方法。看到这里,大概能猜到内部实现了,还是和$set一样,manualobserver,我们来验证constmethodsToPatch=['push','pop','shift','unshift','splice','排序','反向']methodsToPatch。forEach(function(method){//缓存原始数组constoriginal=arrayProto[method]//def使用Object.defineProperty重新定义属性def(arrayMethods,method,functionmutator(...args){constresult=original.apply(this,args)//调用原生数组方法constob=this.__ob__//ob是observe实例observe可以响应letinsertedswitch(method){//push和unshift方法会增加索引array,但是add索引位需要手动观察case'push':case'unshift':inserted=argsbreak//同样,splice的第三个参数,是新加的值,也需要手动观察case'splice':inserted=args.slice(2)break}//其余方法在原始索引上更新,初始化时已经观察到if(inserted)ob.observeArray(inserted)//dep通知所有订阅者触发回调ob.dep.notify()返回结果})})比较1一个优秀的开源框架本身就是一个不断打破和再造的过程,上面已经做了一些铺垫。现在我们简单总结一下,Proxy作为一个新的标准,将会被浏览器厂商不断优化。Proxy可以观察到的类型比defineProperty更丰富。Proxy不兼容IE,没有polyfill,defineProperty可以支持IE9Object。definedProperty是被劫持对象的属性,新元素需要重新defineProperty,Proxy劫持了整个对象,不需要特殊处理。在使用defineProperty的时候,我们修改原来的obj对象可以触发拦截,而使用proxy,必须修改代理对象,即Proxy的实例可以触发拦截参考https://zh.wikipedia.org/wiki/https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxyhttps://es6.ruanyifeng.com/#docs/proxy#Proxy-revocablehttps://youngzhang08.github.io/最近的高效终端命令行工具-ForYourTerminal美格蓉基于Vue实现一个有趣的小游戏