初探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 相关文章推荐
JavaScript实现x秒后自动跳转到一个页面
Jan 03 Javascript
D3.js 从P元素的创建开始(显示可加载数据)
Oct 30 Javascript
JS实现的生成随机数的4个函数分享
Feb 11 Javascript
JS判断字符串字节数并截取长度的方法
Mar 05 Javascript
jQuery Ajax使用FormData上传文件和其他数据后端web.py获取
Jun 11 jQuery
Vue的Flux框架之Vuex状态管理器
Jul 30 Javascript
layui 数据表格 根据值(1=业务,2=机构)显示中文名称示例
Oct 26 Javascript
基于leaflet.js实现修改地图主题样式的流程分析
May 15 Javascript
javascript实现移动端红包雨页面
Jun 23 Javascript
Vue实现购物车基本功能
Nov 08 Javascript
详解React中共享组件逻辑的三种方式
Feb 02 Javascript
关于React Native 无法链接模拟器的问题
Jun 21 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中目录,文件操作详谈
2007/03/19 PHP
PHP生成UTF8文件的方法
2010/05/15 PHP
采用PHP函数memory_get_usage获取PHP内存清耗量的方法
2011/12/06 PHP
使用php实现截取指定长度
2013/08/06 PHP
多个PHP中文字符串截取函数
2013/11/12 PHP
自己写的兼容低于PHP 5.5版本的array_column()函数
2014/10/24 PHP
[原创]php逐行读取txt文件写入数组的方法
2015/07/02 PHP
Yii2实现ajax上传图片插件用法
2016/04/28 PHP
PHP实现简易用户登录系统
2020/07/10 PHP
硬盘浏览程序,保存成网页格式便可使用
2006/12/03 Javascript
一段利用WSH修改和查看IP配置的代码
2008/05/11 Javascript
jQuery 下拉列表 二级联动插件分享
2012/03/29 Javascript
extjs表格文本启用选择复制功能具体实现
2013/10/11 Javascript
javascript中interval与setTimeOut的区别示例介绍
2014/03/14 Javascript
[原创]推荐10款最热门jQuery UI框架
2014/08/19 Javascript
Jquery修改image的src属性,图片不加载问题的解决方法
2016/05/17 Javascript
AngularJs 指令详解及示例代码
2016/09/01 Javascript
微信js-sdk上传与下载图片接口用法示例
2016/10/12 Javascript
BootStrap select2 动态改变值的方法
2017/02/10 Javascript
微信小程序中使用javascript 回调函数
2017/05/11 Javascript
Angular中ng-repeat与ul li的多层嵌套重复问题
2017/07/24 Javascript
Vue-不允许嵌套式的渲染方法
2018/09/13 Javascript
Vue2.x Todo之自定义指令实现自动聚焦的方法
2019/01/08 Javascript
NodeJS 文件夹拷贝以及删除功能
2019/09/03 NodeJs
vue实现设置载入动画和初始化页面动画效果
2019/10/28 Javascript
Python格式化字符串f-string概览(小结)
2019/06/18 Python
Python requests模块安装及使用教程图解
2020/06/30 Python
国际知名设计师时装商店:Coggles
2016/09/05 全球购物
Spartoo葡萄牙鞋类网站:线上销售鞋履与时尚配饰
2017/01/11 全球购物
Merchant 1948澳大利亚:新西兰领先的鞋类和靴子供应商
2018/03/24 全球购物
俄罗斯便宜的在线服装商店:GroupPrice
2020/04/10 全球购物
实现strstr功能,即在父串中寻找子串首次出现的位置
2016/08/05 面试题
新文化运动的基本口号
2014/06/21 职场文书
人生哲理妙语30条:淡写流年,笑过人生
2019/09/04 职场文书
Python快速优雅的批量修改Word文档样式
2021/05/20 Python
Golang MatrixOne使用介绍和汇编语法
2022/04/19 Golang