初探 Vue3.0 中的一大亮点——Proxy !

前言

 

 

 

不久前,也就是11月14日-16日于多伦多举办的 VueConf TO 2018 大会上,尤雨溪发表了名为 Vue3.0 Updates 的主题演讲,对 Vue3.0 的更新计划、方向进行了详细阐述(感兴趣的小伙伴可以看看完整的 PPT),表示已经放弃使用了 Object.defineProperty,而选择了使用更快的原生 Proxy !!

这将会消除了之前 Vue2.x 中基于 Object.defineProperty 的实现所存在的很多限制:无法监听 属性的添加和删除数组索引和长度的变更,并可以支持 MapSetWeakMap 和 WeakSet

做为一个 “前端工程师” ,有必要安利一波 Proxy !!

什么是 Proxy?

MDN 上是这么描述的——Proxy对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等)。

官方的描述总是言简意赅,以至于不明觉厉...

其实就是在对目标对象的操作之前提供了拦截,可以对外界的操作进行过滤和改写,修改某些操作的默认行为,这样我们可以不直接操作对象本身,而是通过操作对象的代理对象来间接来操作对象,达到预期的目的~

什么?还没表述清楚?下面我们看个例子,就一目了然了~

 
  1. let obj = {

  2. a : 1

  3. }

  4. let proxyObj = new Proxy(obj,{

  5. get : function (target,prop) {

  6. return prop in target ? target[prop] : 0

  7. },

  8. set : function (target,prop,value) {

  9. target[prop] = 888;

  10. }

  11. })

  12.  
  13. console.log(proxyObj.a); // 1

  14. console.log(proxyObj.b); // 0

  15.  
  16. proxyObj.a = 666;

  17. console.log(proxyObj.a) // 888

  18.  
  19. 复制代码

上述例子中,我们事先定义了一个对象 obj , 通过 Proxy 构造器生成了一个 proxyObj 对象,并对其的 set(写入) 和 get (读取) 行为重新做了修改。

当我们访问对象内原本存在的属性时,会返回原有属性内对应的值,如果试图访问一个不存在的属性时,会返回0 ,即我们访问 proxyObj.a 时,原本对象中有 a 属性,因此会返回 1 ,当我们试图访问对象中不存在的 b 属性时,不会再返回 undefined ,而是返回了 0 ,当我们试图去设置新的属性值的时候,总是会返回 888 ,因此,即便我们对 proxyObj.a 赋值为 666 ,但是并不会生效,依旧会返回 888!

语法

ES6 原生提供的 Proxy 语法很简单,用法如下:

let proxy = new Proxy(target, handler);

参数 target 是用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理), 参数 handler 也是一个对象,其属性是当执行一个操作时定义代理的行为的函数,也就是自定义的行为。

Proxy 的基本用法就如同上面这样,不同的是 handler 对象的不同,handler 可以是空对象 {} ,则表示对 proxy 操作就是对目标对象 target 操作,即:

 
  1. let obj = {}

  2.  
  3. let proxyObj = new Proxy(obj,{})

  4.  
  5. proxyObj.a = 1;

  6. proxyObj.fn = function () {

  7. console.log('it is a function')

  8. }

  9.  
  10. console.log(proxyObj.a); // 1

  11. console.log(obj.a); // 1

  12. console.log(obj.fn()) // it is a function

  13. 复制代码

但是要注意的是,handler 不能 设置为 null ,会抛出一个错误——Cannot create proxy with a non-object as target or handler

要想 Proxy 起作用,我们就不能去操作原来对象的对象,也就是目标对象 target (上例是 obj 对象 ),必须针对的是 Proxy 实例(上例是 proxyObj 对象)进行操作,否则达不到预期的效果,以刚开始的例子来看,我们设置 get 方法后,试图继续从原对象 obj 中读取一个不存在的属性 b , 结果依旧返回 undefined :

 
  1. console.log(proxyObj.b); // 0

  2. console.log(obj.b); // undefined

  3. 复制代码

对于可以设置、但没有设置拦截的操作,则对 proxy 对象的处理结果也同样会作用于原来的目标对象 target 上,怎么理解呢?还是以刚开始的例子来看,我们重新定义了 set 方法,所有的属性设置都返回了 888 , 并没有对某个特殊的属性(这里指的是 obj 的 a 属性 )做特殊的拦截或处理,那么通过 proxyObj.a = 666 操作后的结果同样也会作用于原来目标对象(obj 对象)上,因此 obj 对象的 a 的值也将会变为 888 !

 
  1. proxyObj.a = 666;

  2. console.log( proxyObj.a); // 888

  3. console.log( obj.a); // 888

  4. 复制代码

API

ES6 中 Proxy 目前提供了 13 种可代理操作,下面我对几个比较常用的 api 做一些归纳和整理,想要了解其他方法的同学可自行去官网查阅 :

--handler.get(target,property,receiver)

用于拦截对象的读取属性操作,target 是指目标对象,property 是被获取的属性名 , receiver 是 Proxy 或者继承 Proxy 的对象,一般情况下就是 Proxy 实例。

 
  1. let proxy = new Proxy({},{

  2. get : function (target,prop) {

  3. console.log(`get ${prop}`);

  4. return 10;

  5. }

  6. })

  7.  
  8. console.log(proxy.a) // get a

  9. // 10

  10. 复制代码

我们拦截了一个空对象的 读取get操作, 当获取其内部的属性是,会输出 get ${prop} , 并返回 10 ;

 
  1. let proxy = new Proxy({},{

  2. get : function (target,prop,receiver) {

  3. return receiver;

  4. }

  5. })

  6.  
  7. console.log(proxy.a) // Proxy{}

  8. console.log(proxy.a === proxy) //true

  9. 复制代码

上述 proxy 对象的 a 属性是由 proxy 对象提供的,所以 receiver 指向 proxy 对象,因此 proxy.a === proxy 返回的是 true

要注意,如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同,也就是不能对其进行修改,否则会抛出异常~

 
  1. let obj = {};

  2. Object.defineProperty(obj, "a", {

  3. configurable: false,

  4. enumerable: false,

  5. value: 10,

  6. writable: false

  7. });

  8.  
  9. let proxy = new Proxy(obj,{

  10. get : function (target,prop) {

  11. return 20;

  12. }

  13. })

  14.  
  15. console.log(proxy.a) // Uncaught TypeError

  16.  
  17. 复制代码

上述 obj 对象中的 a 属性不可写,不可配置,我们通过 Proxy 创建了一个 proxy 的实例,并拦截了它的 get 操作,当我们输出 proxy.a时会抛出异常,此时,如果我们将 get 方法的返回值修改跟目标属性的值相同时,也就是 10 , 就可以消除异常~

--handler.set(target, property, value, receiver)

用于拦截设置属性值的操作,参数于 get 方法相比,多了一个 value ,即要设置的属性值~

严格模式下,set方法需要返回一个布尔值,返回 true 代表此次设置属性成功了,如果返回false且设置属性操作失败,并且会抛出一个TypeError

 
  1. let proxy = new Proxy({},{

  2. set : function (target,prop,value) {

  3. if( prop === 'count' ){

  4. if( typeof value === 'number'){

  5. console.log('success')

  6. target[prop] = value;

  7. }else{

  8. throw new Error('The variable is not an integer')

  9. }

  10. }

  11. }

  12. })

  13.  
  14. proxy.count = '10'; // The variable is not an integer

  15.  
  16. proxy.count = 10; // success

  17. 复制代码

上述我们通过修改 set方法,对 目标对象中的 count 属性赋值做了限制,我们要求 count 属性赋值必须是一个 number 类型的数据,如果不是,就返回一个错误 The variable is not an integer,我们第一次为 count 赋值字符串 '10' , 抛出异常,第二次赋值为数字 10 , 打印成功,因此,我们可以用 set 方法来做一些数据校验!

同样,如果目标属性是不可写及不可配置的,则不能改变它的值,即赋值无效,如下:

 
  1. let obj = {};

  2. Object.defineProperty(obj, "count", {

  3. configurable: false,

  4. enumerable: false,

  5. value: 10,

  6. writable: false

  7. });

  8.  
  9. let proxy = new Proxy(obj,{

  10. set : function (target,prop,value) {

  11. target[prop] = 20;

  12. }

  13. })

  14.  
  15. proxy.count = 20 ;

  16. console.log(proxy.count) // 10

  17. 复制代码

上述 obj 对象中的 count 属性,我们设置它不可被修改,并且默认值,我们给定为 10 ,那么即使给其赋值为 20 ,结果仍旧没有变化!

--handler.apply(target, thisArg, argumentsList)

用于拦截函数的调用,共有三个参数,分别是目标对象(函数)target,被调用时的上下文对象 thisArg 以及被调用时的参数数组 argumentsList,该方法可以返回任何值。

target 必须是是一个函数对象,否则将抛出一个TypeError

 
  1. function sum(a, b) {

  2. return a + b;

  3. }

  4.  
  5. const handler = {

  6. apply: function(target, thisArg, argumentsList) {

  7. console.log(`Calculate sum: ${argumentsList}`);

  8. return target(argumentsList[0], argumentsList[1]) * 2;

  9. }

  10. };

  11.  
  12. let proxy = new Proxy(sum, handler);

  13.  
  14. console.log(sum(1, 2)); // 3

  15. console.log(proxy(1, 2)); // Calculate sum:1,2

  16. // 6

  17. 复制代码

实际上,apply 还会拦截目标对象的 Function.prototype.apply() 和 Function.prototype.call(),以及 Reflect.apply() 操作,如下:

 
  1. console.log(proxy.call(null, 3, 4)); // Calculate sum:3,4

  2. // 14

  3.  
  4. console.log(Reflect.apply(proxy, null, [5, 6])); // Calculate sum: 5,6

  5. // 22

  6. 复制代码

--handler.construct(target, argumentsList, newTarget)

construct 用于拦截 new 操作符,为了使 new 操作符在生成的 Proxy对象上生效,用于初始化代理的目标对象自身必须具有[[Construct]]内部方法;它接收三个参数,目标对象 target ,构造函数参数列表 argumentsList 以及最初实例对象时,new 命令作用的构造函数,即下面例子中的 p

 
  1. let p = new Proxy(function() {}, {

  2. construct: function(target, argumentsList, newTarget) {

  3. console.log(newTarget === p ); // true

  4. console.log('called: ' + argumentsList.join(', ')); // called:1,2

  5. return { value: ( argumentsList[0] + argumentsList[1] )* 10 };

  6. }

  7. });

  8.  
  9. console.log(new p(1,2).value); // 30

  10. 复制代码

另外,该方法必须返回一个对象,否则会抛出异常!

 
  1. var p = new Proxy(function() {}, {

  2. construct: function(target, argumentsList, newTarget) {

  3. return 2

  4. }

  5. });

  6.  
  7. console.log(new p(1,2)); // Uncaught TypeError

  8. 复制代码

--handler.has(target,prop)

has方法可以看作是针对 in 操作的钩子,当我们判断对象是否具有某个属性时,这个方法会生效,典型的操作就是 in ,改方法接收两个参数 目标对象 target 和 要检查的属性 prop,并返回一个 boolean 值。

 
  1. let p = new Proxy({}, {

  2. has: function(target, prop) {

  3. if( prop[0] === '_' ) {

  4. console.log('it is a private property')

  5. return false;

  6. }

  7. return true;

  8. }

  9. });

  10.  
  11. console.log('a' in p); // true

  12. console.log('_a' in p ) // it is a private property

  13. // false

  14.  
  15. 复制代码

上述例子中,我们用 has 方法隐藏了属性以下划线_开头的私有属性,这样在判断时候就会返回 false,从而不会被 in 运算符发现~

要注意,如果目标对象的某一属性本身不可被配置,则该属性不能够被代理隐藏,如果目标对象为不可扩展对象,则该对象的属性不能够被代理隐藏,否则将会抛出 TypeError

 
  1. let obj = { a : 1 };

  2.  
  3. Object.preventExtensions(obj); // 让一个对象变的不可扩展,也就是永远不能再添加新的属性

  4.  
  5. let p = new Proxy(obj, {

  6. has: function(target, prop) {

  7. return false;

  8. }

  9. });

  10.  
  11. console.log('a' in p); // TypeError is thrown

  12. 复制代码

数据绑定

上面介绍了这么多,也算是对 Proxy 又来一个初步的了解,那么我们就可以利用 Proxy 手动实现一个极其简单数据的双向绑定(Object.defineProperty() 的实现方式可以参考我上篇文章的末尾有涉及到)~

主要看功能的实现,所以布局方面我就随手一挥了~

页面结构如下:

 
  1. <!--html-->

  2. <div id="app">

  3. <h3 id="paragraph"></h3>

  4. <input type="text" id="input"/>

  5. </div>

  6. 复制代码

主要还是得看逻辑部分:

 
  1. //获取段落的节点

  2. const paragraph = document.getElementById('paragraph');

  3. //获取输入框节点

  4. const input = document.getElementById('input');

  5.  
  6. //需要代理的数据对象

  7. const data = {

  8. text: 'hello world'

  9. }

  10.  
  11. const handler = {

  12. //监控 data 中的 text 属性变化

  13. set: function (target, prop, value) {

  14. if ( prop === 'text' ) {

  15. //更新值

  16. target[prop] = value;

  17. //更新视图

  18. paragraph.innerHTML = value;

  19. input.value = value;

  20. return true;

  21. } else {

  22. return false;

  23. }

  24. }

  25. }

  26.  
  27. //添加input监听事件

  28. input.addEventListener('input', function (e) {

  29. myText.text = e.target.value; //更新 myText 的值

  30. }, false)

  31.  
  32. //构造 proxy 对象

  33. const myText = new Proxy(data,handler);

  34.  
  35. //初始化值

  36. myText.text = data.text;

  37. 复制代码

上述我们通过Proxy 创建了 myText 实例,通过拦截 myText 中 text 属性 set 方法,来更新视图变化,实现了一个极为简单的 双向数据绑定~

总结

说了这么多 , Proxy 总算是入门了,虽然它的语法很简单,但是要想实际发挥出它的价值,可不是件容易的事,再加上其本身的 Proxy的兼容性方面的问题,所以我们实际应用开发中使用的场景的并不是很多,但不代表它不实用,在我看来,可以利用它进行数据的二次处理、可以进行数据合法性的校验,甚至还可以进行函数的代理,更多有用的价值等着你去开发呢~

况且,Vue3.0 都已经准备发布了,你还不打算让学习一下?

加油!



转载:https://juejin.im/post/5bfcbab0518825741e7bd67f

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值