初探Vue3.0 中的一大亮点Proxy的使用


Posted in Javascript onDecember 06, 2018

前言

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

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

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

什么是 Proxy?

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

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

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

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

let obj = {
   a : 1
  }
  let proxyObj = new Proxy(obj,{
    get : function (target,prop) {
      return prop in target ? 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 语法很简单,用法如下:

let proxy = new Proxy(target, handler);

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

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

let obj = {}
  
  let proxyObj = new Proxy(obj,{})
  
  proxyObj.a = 1;
  proxyObj.fn = function () {
    console.log('it is a function')
  }

  console.log(proxyObj.a); // 1
  console.log(obj.a);   // 1
  console.log(obj.fn())  // it is a function

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

要想 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 实例。

let proxy = new Proxy({},{
  get : function (target,prop) {
    console.log(`get ${prop}`);
    return 10;
  }
})
  
console.log(proxy.a)  // get a
            // 10

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

let proxy = new Proxy({},{
  get : function (target,prop,receiver) {
      return receiver;
    }
  })

console.log(proxy.a)  // Proxy{}
console.log(proxy.a === proxy) //true

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

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

let obj = {};
Object.defineProperty(obj, "a", {
 configurable: false,
 enumerable: false,
 value: 10,
 writable: false
});

let proxy = new Proxy(obj,{
  get : function (target,prop) {
    return 20;
  }
})

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

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

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

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

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

let proxy = new Proxy({},{
  set : function (target,prop,value) {
    if( prop === 'count' ){
      if( typeof value === 'number'){
        console.log('success')
       target[prop] = value;
      }else{
       throw new Error('The variable is not an integer')
      }
    }
  }
})
  
 proxy.count = '10';  // The variable is not an integer
 
 proxy.count = 10;   // success

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

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

let obj = {};
Object.defineProperty(obj, "count", {
  configurable: false,
  enumerable: false,
  value: 10,
  writable: false
});

let proxy = new Proxy(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;

function sum(a, b) {
 return a + b;
}

const handler = {
  apply: function(target, thisArg, argumentsList) {
   console.log(`Calculate sum: ${argumentsList}`); 
   return target(argumentsList[0], argumentsList[1]) * 10;
  }
};

let proxy = new Proxy(sum, handler);

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

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

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

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

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

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

let p = new Proxy(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(new p(1,2).value);   // 30

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

var p = new Proxy(function() {}, {
  construct: function(target, argumentsList, newTarget) {
   return 2
  }
});

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

--handler.has(target,prop)

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

let p = new Proxy({}, {
  has: function(target, prop) {
   if( prop[0] === '_' ) {
   console.log('it is a private property')
   return false;
   }
   return true;
  }
});

console.log('a' in p);   // true
console.log('_a' in p )   // it is a private property
              // false

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

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

let obj = { a : 1 };

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

let p = new Proxy(obj, {
 has: function(target, prop) {
 return false;
 }
});

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

数据绑定

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

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

页面结构如下:

<!--html-->
<div id="app">
  <h3 id="paragraph"></h3>
  <input type="text" id="input"/>
</div>

主要还是得看逻辑部分:

//获取段落的节点
const paragraph = document.getElementById('paragraph');
//获取输入框节点
const input = document.getElementById('input');
  
//需要代理的数据对象
const data = {
 text: 'hello world'
}

const handler = {
 //监控 data 中的 text 属性变化
 set: function (target, prop, value) {
   if ( prop === 'text' ) {
        //更新值
        target[prop] = value;
        //更新视图
        paragraph.innerHTML = value;
        input.value = value;
        return true;
   } else {
   return false;
   }
 }
}

//添加input监听事件
input.addEventListener('input', function (e) {
  myText.text = e.target.value;  //更新 myText 的值
}, false)

//构造 proxy 对象
const myText = new Proxy(data,handler);

//初始化值
myText.text = data.text;

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

总结

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

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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js 获取radio按钮值的实例
Aug 17 Javascript
页面加载完成后再执行JS的jquery写法以及区别说明
Feb 22 Javascript
JS实现进入页面时渐变背景色的方法
Feb 25 Javascript
javascript包装对象实例分析
Mar 27 Javascript
JS模式之简单的订阅者和发布者模式完整实例
Jun 30 Javascript
深入分析jQuery的ready函数是如何工作的(工作原理)
Dec 17 Javascript
原生javascript上传图片带进度条【实例分享】
Apr 06 Javascript
js获取一组日期中最近连续的天数
May 25 Javascript
提升node.js中使用redis的性能遇到的问题及解决方法
Oct 30 Javascript
vue与django集成打包的实现方法
Nov 11 Javascript
文章或博客自动生成章节目录索引(支持三级)的实现代码
May 10 Javascript
如何在vue中使用jointjs过程解析
May 29 Javascript
vue中v-for通过动态绑定class实现触发效果
Dec 06 #Javascript
vue通过指令(directives)实现点击空白处收起下拉框
Dec 06 #Javascript
Vue-CLI3.x 设置反向代理的方法
Dec 06 #Javascript
JavaScript中引用vs复制示例详析
Dec 06 #Javascript
使用jQuery动态设置单选框的选中效果
Dec 06 #jQuery
express+vue+mongodb+session 实现注册登录功能
Dec 06 #Javascript
如何使用puppet替换文件中的string
Dec 06 #Javascript
You might like
PHP编程基本语法快速入门手册
2016/01/07 PHP
thinkphp中的url跳转用法分析
2016/07/12 PHP
THinkPHP获取客户端IP与IP地址查询的方法
2016/11/14 PHP
JS Array对象入门分析
2008/10/30 Javascript
jQuery Ajax文件上传(php)
2009/06/16 Javascript
深入分析JSONP跨域的原理
2014/12/10 Javascript
JavaScript中pop()方法的使用教程
2015/06/09 Javascript
浅谈javascript原型链与继承
2015/07/13 Javascript
JS实现的论坛Ajax打分效果完整实例
2015/10/31 Javascript
AngularJs学习第五篇从Controller控制器谈谈$scope作用域
2016/06/08 Javascript
Angular4学习教程之DOM属性绑定详解
2018/01/04 Javascript
jQuery实现下拉菜单动态添加数据点击滑出收起其他功能
2018/06/14 jQuery
JavaScript面向对象程序设计创建对象的方法分析
2018/08/13 Javascript
element-ui 中的table的列隐藏问题解决
2018/08/24 Javascript
详解微信小程序-canvas绘制文字实现自动换行
2019/04/26 Javascript
js实现点击生成随机div
2020/01/16 Javascript
Python中实现的RC4算法
2015/02/14 Python
Python smallseg分词用法实例分析
2015/05/28 Python
Python基于PyGraphics包实现图片截取功能的方法
2017/12/21 Python
利用python实现PSO算法优化二元函数
2019/11/13 Python
python将时分秒转换成秒的实例
2019/12/07 Python
Python破解BiliBili滑块验证码的思路详解(完美避开人机识别)
2020/02/17 Python
django有哪些好处和优点
2020/09/01 Python
HTML5 解决苹果手机不能自动播放音乐问题
2017/12/27 HTML / CSS
台湾SHOPRO购物行家:亚洲首创影视.3C.家电.优质购物平台
2018/05/07 全球购物
俄罗斯GamePark游戏商店网站:购买游戏、游戏机和配件
2020/03/13 全球购物
最新党员的自我评价分享
2013/11/04 职场文书
金融专业大学生职业生涯规划范文
2014/01/16 职场文书
公益活动邀请函
2014/02/05 职场文书
大型晚会策划方案
2014/02/06 职场文书
消防安全责任书
2014/04/14 职场文书
《青海高原一株柳》教学反思
2014/04/25 职场文书
爱耳日宣传活动总结
2014/07/05 职场文书
预备党员自我批评思想汇报
2014/10/10 职场文书
毕业生个人总结
2015/02/28 职场文书
Maven学习----Maven安装与环境变量配置教程
2021/06/29 Java/Android