初探Vue3.0 中的一大亮点Proxy的使用
前言
不久前,也就是11月14日-16日于多伦多举办的VueConfTO2018大会上,尤雨溪发表了名为Vue3.0Updates的主题演讲,对Vue3.0的更新计划、方向进行了详细阐述,表示已经放弃使用了Object.defineProperty,而选择了使用更快的原生Proxy!!
这将会消除了之前Vue2.x中基于Object.defineProperty的实现所存在的很多限制:无法监听属性的添加和删除、数组索引和长度的变更,并可以支持Map、Set、WeakMap和WeakSet!
做为一个“前端工程师”,有必要安利一波Proxy!!
什么是Proxy?
MDN上是这么描述的——Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。
官方的描述总是言简意赅,以至于不明觉厉...
其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~
什么?还没表述清楚?下面我们看个例子,就一目了然了~
letobj={ a:1 } letproxyObj=newProxy(obj,{ get:function(target,prop){ returnpropintarget?target[prop]:0 }, set:function(target,prop,value){ target[prop]=888; } }) console.log(proxyObj.a);//1 console.log(proxyObj.b);//0 proxyObj.a=666; console.log(proxyObj.a)//888
上述例子中,我们事先定义了一个对象obj,通过Proxy构造器生成了一个proxyObj对象,并对其的set(写入)和get(读取)行为重新做了修改。
当我们访问对象内原本存在的属性时,会返回原有属性内对应的值,如果试图访问一个不存在的属性时,会返回0,即我们访问proxyObj.a时,原本对象中有a属性,因此会返回1,当我们试图访问对象中不存在的b属性时,不会再返回undefined,而是返回了0,当我们试图去设置新的属性值的时候,总是会返回888,因此,即便我们对proxyObj.a赋值为666,但是并不会生效,依旧会返回888!
语法
ES6原生提供的Proxy语法很简单,用法如下:
letproxy=newProxy(target,handler);
参数target是用Proxy包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理),参数handler也是一个对象,其属性是当执行一个操作时定义代理的行为的函数,也就是自定义的行为。
Proxy的基本用法就如同上面这样,不同的是handler对象的不同,handler可以是空对象{},则表示对proxy操作就是对目标对象target操作,即:
letobj={} letproxyObj=newProxy(obj,{}) proxyObj.a=1; proxyObj.fn=function(){ console.log('itisafunction') } console.log(proxyObj.a);//1 console.log(obj.a);//1 console.log(obj.fn())//itisafunction
但是要注意的是,handler不能设置为null,会抛出一个错误——Cannotcreateproxywithanon-objectastargetorhandler!
要想Proxy起作用,我们就不能去操作原来对象的对象,也就是目标对象target(上例是obj对象),必须针对的是Proxy实例(上例是proxyObj对象)进行操作,否则达不到预期的效果,以刚开始的例子来看,我们设置get方法后,视图继续从原对象obj中读取一个不存在的属性b,结果依旧返回undefined:
console.log(proxyObj.b);//1 console.log(obj.b);//undefined
对于可以设置、但没有设置拦截的操作,则对proxy对象的处理结果也同样会作用于原来的目标对象target上,怎么理解呢?还是以刚开始的例子来看,我们重新定义了set方法,所有的属性设置都返回了888,并没有对某个特殊的属性(这里指的是obj的a属性)做特殊的拦截或处理,那么通过proxyObj.a=666操作后的结果同样也会作用于原来目标对象(obj对象)上,因此obj对象的a的值也将会变为888!
proxyObj.a=666; console.log(proxyObj.a);//888 console.log(obj.a);//888
API
ES6中Proxy目前提供了13种可代理操作,下面我对几个比较常用的api做一些归纳和整理,想要了解其他方法的同学可自行去官网查阅:
--handler.get(target,property,receiver)
用于拦截对象的读取属性操作,target是指目标对象,property是被获取的属性名,receiver是Proxy或者继承Proxy的对象,一般情况下就是Proxy实例。
letproxy=newProxy({},{ get:function(target,prop){ console.log(`get${prop}`); return10; } }) console.log(proxy.a)//geta //10
我们拦截了一个空对象的读取get操作,当获取其内部的属性是,会输出get${prop},并返回10;
letproxy=newProxy({},{ get:function(target,prop,receiver){ returnreceiver; } }) console.log(proxy.a)//Proxy{} console.log(proxy.a===proxy)//true
上述proxy对象的a属性是由proxy对象提供的,所以receiver指向proxy对象,因此proxy.a===proxy返回的是true。
要注意,如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同,也就是不能对其进行修改,否则会抛出异常~
letobj={}; Object.defineProperty(obj,"a",{ configurable:false, enumerable:false, value:10, writable:false }); letproxy=newProxy(obj,{ get:function(target,prop){ return20; } }) console.log(proxy.a)//UncaughtTypeError
上述obj对象中的a属性不可写,不可配置,我们通过Proxy创建了一个proxy的实例,并拦截了它的get操作,当我们输出proxy.a时会抛出异常,此时,如果我们将get方法的返回值修改跟目标属性的值相同时,也就是10,就可以消除异常~
--handler.set(target,property,value,receiver)
用于拦截设置属性值的操作,参数于get方法相比,多了一个value,即要设置的属性值~
在严格模式下,set方法需要返回一个布尔值,返回true代表此次设置属性成功了,如果返回false且设置属性操作失败,并且会抛出一个TypeError。
letproxy=newProxy({},{ set:function(target,prop,value){ if(prop==='count'){ if(typeofvalue==='number'){ console.log('success') target[prop]=value; }else{ thrownewError('Thevariableisnotaninteger') } } } }) proxy.count='10';//Thevariableisnotaninteger proxy.count=10;//success
上述我们通过修改set方法,对目标对象中的count属性赋值做了限制,我们要求count属性赋值必须是一个number类型的数据,如果不是,就返回一个错误Thevariableisnotaninteger,我们第一次为count赋值字符串'10',抛出异常,第二次赋值为数字10,打印成功,因此,我们可以用set方法来做一些数据校验!
同样,如果目标属性是不可写及不可配置的,则不能改变它的值,即赋值无效,如下:
letobj={}; Object.defineProperty(obj,"count",{ configurable:false, enumerable:false, value:10, writable:false }); letproxy=newProxy(obj,{ set:function(target,prop,value){ target[prop]=20; } }) proxy.count=20; console.log(proxy.count)//10
上述obj对象中的count属性,我们设置它不可被修改,并且默认值,我们给定为10,那么即使给其赋值为20,结果仍旧没有变化!
--handler.apply(target,thisArg,argumentsList)
用于拦截函数的调用,共有三个参数,分别是目标对象(函数)target,被调用时的上下文对象thisArg以及被调用时的参数数组argumentsList,该方法可以返回任何值。
target必须是是一个函数对象,否则将抛出一个TypeError;
functionsum(a,b){ returna+b; } consthandler={ apply:function(target,thisArg,argumentsList){ console.log(`Calculatesum:${argumentsList}`); returntarget(argumentsList[0],argumentsList[1])*10; } }; letproxy=newProxy(sum,handler); console.log(sum(1,2));//3 console.log(proxy(1,2));//Calculatesum:1,2 //6
实际上,apply还会拦截目标对象的Function.prototype.apply()和Function.prototype.call(),以及Reflect.apply()操作,如下:
console.log(proxy.call(null,3,4));//Calculatesum:3,4 //14 console.log(Reflect.apply(proxy,null,[5,6]));//Calculatesum:5,6 //22
--handler.construct(target,argumentsList,newTarget)
construct用于拦截new操作符,为了使new操作符在生成的Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法;它接收三个参数,目标对象target,构造函数参数列表argumentsList以及最初实例对象时,new命令作用的构造函数,即下面例子中的p。
letp=newProxy(function(){},{ construct:function(target,argumentsList,newTarget){ console.log(newTarget===p);//true console.log('called:'+argumentsList.join(','));//called:1,2 return{value:(argumentsList[0]+argumentsList[1])*10}; } }); console.log(newp(1,2).value);//30
另外,该方法必须返回一个对象,否则会抛出异常!
varp=newProxy(function(){},{ construct:function(target,argumentsList,newTarget){ return2 } }); console.log(newp(1,2));//UncaughtTypeError
--handler.has(target,prop)
has方法可以看作是针对in操作的钩子,当我们判断对象是否具有某个属性时,这个方法会生效,典型的操作就是in,改方法接收两个参数目标对象target和要检查的属性prop,并返回一个boolean值。
letp=newProxy({},{ has:function(target,prop){ if(prop[0]==='_'){ console.log('itisaprivateproperty') returnfalse; } returntrue; } }); console.log('a'inp);//true console.log('_a'inp)//itisaprivateproperty //false
上述例子中,我们用has方法隐藏了属性以下划线_开头的私有属性,这样在判断时候就会返回false,从而不会被in运算符发现~
要注意,如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏,如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏,否则将会抛出TypeError。
letobj={a:1}; Object.preventExtensions(obj);//让一个对象变的不可扩展,也就是永远不能再添加新的属性 letp=newProxy(obj,{ has:function(target,prop){ returnfalse; } }); console.log('a'inp);//TypeErroristhrown
数据绑定
上面介绍了这么多,也算是对Proxy又来一个初步的了解,那么我们就可以利用Proxy手动实现一个极其简单数据的双向绑定(Object.defineProperty()的实现方式可以参考我上篇文章的末尾有涉及到)~
主要看功能的实现,所以布局方面我就随手一挥了~
页面结构如下:
主要还是得看逻辑部分:
//获取段落的节点 constparagraph=document.getElementById('paragraph'); //获取输入框节点 constinput=document.getElementById('input'); //需要代理的数据对象 constdata={ text:'helloworld' } consthandler={ //监控data中的text属性变化 set:function(target,prop,value){ if(prop==='text'){ //更新值 target[prop]=value; //更新视图 paragraph.innerHTML=value; input.value=value; returntrue; }else{ returnfalse; } } } //添加input监听事件 input.addEventListener('input',function(e){ myText.text=e.target.value;//更新myText的值 },false) //构造proxy对象 constmyText=newProxy(data,handler); //初始化值 myText.text=data.text;
上述我们通过Proxy创建了myText实例,通过拦截myText中text属性set方法,来更新视图变化,实现了一个极为简单的双向数据绑定~
总结
说了这么多,Proxy总算是入门了,虽然它的语法很简单,但是要想实际发挥出它的价值,可不是件容易的事,再加上其本身的Proxy的兼容性方面的问题,所以我们实际应用开发中使用的场景的并不是很多,但不代表它不实用,在我看来,可以利用它进行数据的二次处理、可以进行数据合法性的校验,甚至还可以进行函数的代理,更多有用的价值等着你去开发呢~
况且,Vue3.0都已经准备发布了,你还不打算让学习一下?
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。