不久前,也就是11月14日-16日于多伦多举办的 VueConf TO 2018
大会上,尤雨溪发表了名为 Vue3.0 Updates
的主题演讲,对 Vue3.0
的更新计划、方向进行了详细阐述(感兴趣的小伙伴能够看看完整的 PPT),表示已经放弃使用了 Object.defineProperty
,而选择了使用更快的原生 Proxy
!!html
这将会消除了以前 Vue2.x
中基于 Object.defineProperty
的实现所存在的不少限制:没法监听 属性的添加和删除、数组索引和长度的变动,并能够支持 Map
、Set
、WeakMap
和 WeakSet
!前端
作为一个 “前端工程师” ,有必要安利一波 Proxy
!!api
MDN 上是这么描述的——Proxy
对象用于定义基本操做的自定义行为(如属性查找,赋值,枚举,函数调用等)。数组
官方的描述老是言简意赅,以致于不明觉厉...bash
其实就是在对目标对象的操做以前提供了拦截,能够对外界的操做进行过滤和改写,修改某些操做的默认行为,这样咱们能够不直接操做对象自己,而是经过操做对象的代理对象来间接来操做对象,达到预期的目的~前端工程师
什么?还没表述清楚?下面咱们看个例子,就一目了然了~app
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
(读取) 行为从新作了修改。ide
当咱们访问对象内本来存在的属性时,会返回原有属性内对应的值,若是试图访问一个不存在的属性时,会返回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); // 0
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
复制代码
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]) * 2;
}
};
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
都已经准备发布了,你还不打算让学习一下?
加油!