当前位置: 首页 > Web前端 > JavaScript

逆向操作,用Object.defineProperty覆盖@vue-reactivity

时间:2023-03-27 17:57:14 JavaScript

背景我们都知道vue3重写了响应式代码,使用Proxy劫持数据操作,分离出一个单独的库@vue/reactivity,不局限于vue,可以在任何js代码中使用,但是由于使用了Proxy,Proxycannot使用polyfill为了兼容使得在不支持Proxy的环境下无法使用。这也是vue3不支持ie11的部分原因。部分内容:响应式原理@vue/reactivity与vue2响应式的区别使用Object.defineProperty重写遇到的问题及解决方案代码实现应用场景及限制源码地址:reactivity主要是defObserver。之前我们先简单了解下@vue/reactivity的响应。首先我们在获取数据的时候劫持了一个数据来收集依赖,记录下我们调用了哪个方法,假设是方法effect1调用的。set中设置数据时,get中get中记录的方法触发effect1函数达到监听的目的,effect是一个封装方法,将调用前后的执行栈设置为自身,收集函数执行时的依赖差异vue3与vue2相比,最大的区别是使用ProxyProxy可以比Object.defineProperty有更全面的代理拦截:(虽然Proxy带来了更全面的功能,但是也带来了性能,Proxy其实比Object.defineProperty慢很多)关于ES6Proxy性能的思考get/set劫持未知属性constobj=reactive({});effect(()=>{console.log(obj.name);});obj.name=111;this在Vue2中必须使用set方法来赋值数组元素下标的变化。可以直接使用下标操作数组,直接修改数组lengthconstarr=reactive([]);effect(()=>{console.log(arr[0]);});arr[0]=111;支持deleteobj[key]属性删除的constobj=reactive({name:111,});effect(()=>{console.log(obj.name);});deleteobj.name;obj属性中的key是否存在支持constobj=reactive({});effect(()=>{console.log("name"inobj);});obj.name=111;for(letkeyinobj){}属性遍历ownKeys支持constobj=reactive({});effect(()=>{for(constkeyinobj){console.log(key);}});obj.name=111;支持Map,Set,WeakMap,WeakSet这些是Proxy带来的功能,有一些新的概念或者用法上的变化。独立分包,Vue中不仅可以使用reactive/effect/computed等函数式方法,更灵活地将原始数据和响应数据隔离,还可以通过toRaw获取原始数据,在vue2中,直接在原始数据,功能比较全面reactive/readonly/shallowReactive/shallowReadonly/ref/effectScope,read-only,shallow,基本类型的劫持,function那么如果我们要使用Object.defineProperty,是否可以完成上面的功能呢?你会遇到什么问题?问题及解决方案我们先忽略Proxy和Object.defineProperty的功能差异,因为我们要写的是@vue/reactivity而不是vue2,所以我们需要先解决一些新的概念差异,比如原始数据和响应数据的隔离@vue/reactivity方法,原始数据和响应数据之间存在弱类型引用(WeakMap)。获取对象类型数据时,仍取原始数据。判断是否有对应的响应数据,然后去取。如果存在则生成相应的响应式数据保存并获取。这样,它就被控制在get级别。通过responsivedata得到的永远是responsivetype,通过originalobject得到的永远是originaldata(除非你直接直接赋值responsivetype)propertiesinanoriginalobject),则不能直接使用vue2的源码。根据上面提到的逻辑,写一个最低限度实现的代码来验证逻辑:constproxyMap=newWeakMap();functionreactive(target){//如果当前原始对象已经有对应的响应对象,则返回缓存constexistingProxy=proxyMap.get(target);如果(现有代理){返回现有代理;}常量代理={};for(constkeyintarget){proxyKey(proxy,target,key);}proxyMap.set(target,proxy);返回代理;}functionproxyKey(proxy,target,key){Object.defineProperty(proxy,key,{enumerable:true,configurable:true,get:function(){console.log("get",key);constres=target[key];if(typeofres==="object"){returnreactive(res);}returnres;},set:function(value){console.log("set",key,value);复制代码target[key]=value;},});tryitintheonlineexamplesowedo这里,原始数据和响应数据是隔离的,不管数据层次有多深,我们还面临一个问题,数组呢?数组是通过下标获取的,跟对象的属性不一样。如何隔离它们就是像对象一样劫持数组下标。consttarget=[{deep:{name:1}}];constproxy=[];for(letkeyintarget){proxyKey(proxy,target,key);}在网上的例子中,尝试在上面的代码中加入一个isArray判断,这也决定了我们会一直维护这个数组映射未来。其实也很简单。当数组push/unshift/pop/shift/splice的长度发生变化时,重新映射新增或删除的下标。常量仪器={};//存储重写的方法["push","pop","shift","unshift","splice"].forEach((key)=>{instruments[key]=function(...args){constoldLen=target.length;constres=target[key](...args);constnewLen=target.length;//添加/删除元素if(oldLen!==newLen){if(oldLennewLen){for(leti=newLen;i{console.log(obj.name);});obj.name=2;接下来,在实现上,我们需要修复defineProperty和Proxy的区别。以下区别如下:数组下标改变未知元素劫持元素hash操作元素删除操作元素ownKeys操作数组下标改变数组有点特殊当我们调用unshift在数组开头插入元素时需要trigger通知要更改的数组的每个项目。这在Proxy中是完全支持的,不需要写多余的代码,但是使用defineProperty需要我们兼容计算splice中哪些下标发生了变化,shift,pop,push等操作也需要计算哪些下标发生了变化,然后通知给他们。还有一个缺点:不会监控数组的长度,因为长度属性不能改变。以后可能会考虑用物件代替。而不是数组,但是你不能使用Array.is数组判断:consttarget=[1,2];constproxy=Object.create(target);for(constkintarget){proxyKey(proxy,target,k);}proxyKey(proxy,target,"length");其余的其他操作属于defineProperty的缺陷,我们只能通过添加额外的方法来支持,所以我们添加了set、get、has、del、ownKeys方法(点击方法查看源码实现)来使用constobj=reactive({});effect(()=>{console.log(has(obj,"name"));//确定未知属性});effect(()=>{console.log(get(obj,"name"));//获取未知属性});effect(()=>{for(constkinownKeys(obj)){//遍历未知属性console.log("key:",k);}});set(obj,"name",11111);//设置未知属性del(obj,"name");//删除属性obj本来就是一个空对象,不知道以后会增加什么属性set和del都是vue2中存在的,用来兼容defineProperty的缺陷。setreplaces未知属性的设置getreplaces未知属性的获取delreplacesdeleteobj.namedelete语法hasreplaces'name'inobj判断是否有ownKeys而不是for(constkinobj){}等遍历操作.在遍历对象/数组时,应该使用ownKeys来包装应用场景和限制。目前该功能主要定位为:非vue环境,不支持ProxyOthers语法兼容polyfill,因为老版本的vue2语法不需要改动。如果想使用vue2中的新语法,也可以使用composition-api进行兼容。为什么要做这个东西,原因是我们的应用(小程序)其实还有一些用户环境不支持Proxy,但是我还是想用@vue/reactivity语法对于上面使用的例子,我们也应该知道其局限性相当大,灵活性的成本也很高。如果想要更加灵活,就必须要用一个方法来包裹起来。如果不灵活的话,用法和vue2差别不大,先初始化所有属性时,定义constdata=reactive({list:[],form:{title:"",},});这种方法带来了一种精神上的损失,在使用和设置的时候,必须要考虑这个属性是不是未知属性,要不要用方法来包装。如果你想把所有的设置都用一个方法包装起来,这样的代码哪里都看不到。而且根据木桶效应,一旦你使用了wrapper方式,那么高版本似乎就没有必要自动切换到Proxy劫持了。这样造成的代价无疑是非常大的,有些js语法过于灵活无法支持