Vue中的代理Proxy和$ref
一、 代理Proxy
1. 介绍
1. Proxy又称为代理。在现实生活中,大家对代理二字并不会太陌生,比如某产品的代理。打个比方来说,我们要买一台手机,我们不会直接到一家手机厂去买,会在手机的代理商中买。
2. 在JavaScript中,Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程,即对编程语言进行编程。
3. Proxy可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来代理某些操作,可以译为代理器。
2. 基本语法
1 // @param {Object} target 用来被代理的对象
2 // @param {Object} handler 用来设置代理的对象
3 let proxy = new Proxy(target, handler)
1 Proxy,见名知意,其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:
2 拦截和监视外部对对象的访问
3 降低函数或类的复杂度
4 在复杂操作前对操作进行校验或对所需资源进行管理
5 在支持 Proxy 的浏览器环境中,Proxy 是一个全局对象,可以直接使用。Proxy(target, handler) 是一个构造函数,target 是被代理的对象,handlder 是声明了各类代理操作的对象,最终返回一个代理对象。外界每次通过代理对象访问 target 对象的属性时,就会经过 handler 对象,从这个流程来看,代理对象很类似 middleware(中间件)。那么 Proxy 可以拦截什么操作呢?最常见的就是 get(读取)、set(修改)对象属性等操作,完整的可拦截操作列表请点击这里。此外,Proxy 对象还提供了一个 revoke 方法,可以随时注销所有的代理操作。
一个代理的例子:
1 const target = { name: 'Billy Bob', age: 15 };
2 const handler = { get(target, key, proxy) { const today = new Date();
3 console.log(`GET request made for ${key} at ${today}`);
return Reflect.get(target, key, proxy); } };
4 const proxy = new Proxy(target, handler);
5 proxy.name; // => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)" // => "Billy Bob"
1.Reflect称为反射。它也是ES6中为了操作对象而提供的新的API,用来替代直接调用Object的方法。Reflect是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同。Reflect不是一个函数对象,因此它是不可构造的。
Reflect与大多数全局对象不同,Reflect没有构造函数。你不能将其与一个new运算符一起使用,或者将Reflect对象作为一个函数来调用。
1. 处理器对象handler
处理器对象handler一共提供了14种可代理操作,每种操作的代号(属性名/方法名)和触发这种操作的方式如下:
handler.getPrototypeOf():在读取代理对象的原型时触发该操作,比如在执行Object.getPrototypeOf(proxy)时
handler.setPrototypeOf():在设置代理对象的原型时触发该操作,比如在执行Object.setprototypeOf(proxy, null)时
handler.isExtensible():在判断一个代理对象是否是可扩展时触发该操作,比如在执行Object.isExtensible(proxy)时
handler.preventExtensions():在让一个代理对象不可扩展时触发该操作,比如在执行Object.preventExtensions(proxy)时
handler.getOwnPropertyDescriptor():在获取代理对象某个属性的属性描述时触发该操作,比如在执行Object.getOwnPropertyDescriptor(proxy, 'foo')时
handler.defineProperty():在定义代理对象某个属性时的属性描述时触发该操作,比如在执行Object.defineProperty(proxy,'foo',{})时
handler.has():在判断代理对象是否拥有某个属性时触发该操作,比如在执行'foo' in proxy时
handler.get():在读取代理对象的某个属性时触发该操作,比如在执行proxy.foo时
handler.set():在给代理对象的某个赋值时触发该操作,比如在执行proxy.foo = 1时
handler.deleteProperty():在删除代理对象的某个属性时触发该操作,比如在执行delete proxy.foo时
handler.ownKeys():在获取代理对象的所有属性键时触发该操作,比如在执行Object.getOwnPropertyNames(proxy)时
handler.apply():在调用一个目标对象为函数的代理对象时触发该操作,比如在执行proxy()时
handler.construct():在给一个目标对象为构造函数的代理对象构造实例时触发该操作,比如在执行new proxy()时
Reflect对象拥有对应的可以控制各种元编程任务的静态方法。这些功能和Proxy一一对应。
下面的这些名称你可能看起来很眼熟(因为他们也是Object上的方法):
Reflect.getOwnPropertyDescriptor(..)
Reflect.defineProperty(..)
Reflect.getPrototypeOf(..)
Reflect.setPrototypeOf(..)
Reflect.preventExtensions(..)
Reflect.isExtensible(..)
这些方法和在Object上的同名方法一样。然后,一个区别在于,Object上这么方法的第一个参数是一个对象,Reflect遇到这种情况会扔出一个错误。
3. 陷阱代理
使用set陷阱验证属性
假设创建一个属性值是数字的对象,对象中每新增一个属性都要加以验证,如果不是数字必须抛出错误。为了实现这个任务,可以定义一个set陷阱来覆写设置值的默认特性。
set陷阱接受4个参数:
trapTaqget 用于接收属性(代理的目标)的对象
key 要写入的属性键(字符串或Symbol类型)
value 被写入属性的值
receiver 操作发生的对象(通常是代理)
Reflect.set()是set陷阱对应的反射方法和默认特性,它和set代理陷阱一样也接受相同的4个参数,以方便在陷阱中使用。如果属性已设置陷阱应该返回true,如果未设置则返回false。(Reflect.set()方法基于操作是否成功来返回恰当的值)。
可以使用set陷阱并检查传入的值来验证属性值:
1 //set陷阱并检查传入的值来验证属性值
2 let target = { name: "target" };
3 let proxy = new Proxy(target, {
4 set(trapTarget, key, value, receiver) {
5 // 忽略已有属性,避免影响它们
6 if (!trapTarget.hasOwnProperty(key)) {
7 if (isNaN(value)) { throw new TypeError("Property must be a number."); }
8 } // 添加属性
9 return Reflect.set(trapTarget, key, value, receiver);
10 }
11 });
12 // 添加一个新属性
13 proxy.count = 1;
14 console.log(proxy.count); // => 1
15 console.log(target.count); // => 1 // 你可以为 name 赋一个非数值类型的值,因为该属性已经存在
16 proxy.name = "proxy";
17 console.log(proxy.name); // => "proxy"
18 console.log(target.name); // => "proxy"
// 抛出错误 proxy.anotherName = "proxy";
这段代码定义了一个代理来验证添加到target的新属性,当执行proxy.count=1时,set陷阱被调用,此时trapTarget的值等于target,key等于"count",value等于1,receiver等于proxy。
由于target上没有count属性,因此代理继续将value值传入isNaN(),如果结果是NaN,则证明传入的属性值不是数字,同时也抛出一个错误。在这段代码中,count被设置为1,所以代理调用Reflect.set()方法并传入陷阱接受的4个参数来添加新属性。
proxy.name可以成功被赋值为一个字符串,这是因为target已经拥有一个name属性,但通过调用trapTarget.hasownproperty()方法验证检查后被排除了,所以目标已有的非数字属性仍然可以被操作。
然而,将proxy.anotherName赋值为一个字符串时会抛出错误。目标上没有anotherName属性,所以它的值需要被验证,而由于"Proxy"不是一个数字值,因此抛出错误。
set代理陷阱可以拦截写入属性的操作,get代理陷阱可以拦截读取属性的操作
用get陷阱验证对象结构(Object Shape)
JS有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取属性的值。
1 //用get陷阱验证对象结构(Object Shape)
2 let target = {};
3 console.log(target.name); // => undefined
对象结构是指对象中所有可用属性和方法的集合,JS引擎通过对象结构来优化代码,通常会创建类来表示对象,如果可以安全地假定一个对象将始终具有相同的属性和方法,那么当程序试图访问不存在的属性时会抛出错误。代理让对象结构检验变得简单。
因为只有当读取属性时才会检验属性,所以无论对象中是否存在某个属性,都可以通过get陷阱来检测,它接受3个参数:
trapTarget 被读取属性的源对象(代理的目标)
key 要读取的属性键(字符串或Symbol)
receiver 操作发生的对象(通常是代理)
由于get陷阱不写入值,所以它复刻了set陷阱中除value外的其他3个参数,Reflect.get()也接受同样3个参数并返回属性的默认值。
如果属性在目标上不存在,则使用get陷阱和Reflect.get()时会抛出错误:
let proxy = new Proxy({}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
});
// 添加属性的功能正常
proxy.name = "proxy";
console.log(proxy.name); // => "proxy"
// 读取不存在属性会抛出错误
console.log(proxy.nme); // => 抛出错误
原型代理陷阱
Object.setPrototypeOf()方法被用于作为ES5中的Object.getPrototypeOf()方法的补充。通过代理中的setPrototypeOf陷阱和getPrototypeOf陷阱可以拦截这两个方法的执行过程,在这两种情况下,Object上的方法会调用代理中的同名陷阱来改变方法的行为。
两个陷阱均与代理有关,但具体到方法只与每个陷阱的类型有关,setPrototypeOf陷阱接受以下这些参数:
trapTarget 接受原型设置的对象(代理的目标)
proto 作为原型使用的对象
传入Object.setPrototypeOf()方法和Reflect.setPrototypeOf()方法的均是以上两个参数,另一方面,getPrototypeOf陷阱中的Object.getPrototypeOf()方法和Reflect.getPrototypeOf()方法只接受参数trapTarget。
原型代理陷阱的运行机制
原型代理陷阱有一些限制。首先,getPrototypeOf陷阱必须返回对象或null,否则将导致运行时错误,返回值检查可以确保Object.getPrototypeOf()返回的总是预期的值;其次,在setPrototypeOf陷阱中,如果操作失败则返回的一定是false,此时Object.setPrototypeOf()会抛出错误,如果setPrototypeOf返回了任何不是false的值,那么Object.setPrototypeOf()便假设操作成功。
以下示例通过总是返回null,且不允许改变原型的方式隐藏了代理的原型:
1 let target = {};
2 let proxy = new Proxy(target, {
3 getPrototypeOf(trapTarget) {
4 return null;
5 },
6 setPrototypeOf(trapTarget, proto) {
7 return false;
8 }
9 });
10 let targetProto = Object.getPrototypeOf(target);
11 let proxyProto = Object.getPrototypeOf(proxy);
12 console.log(targetProto === Object.prototype); // => true
13 console.log(proxyProto === Object.prototype); // => false
14 console.log(proxyProto); // => null
// 成功
15 Object.setPrototypeOf(target, {});
// 抛出错误
16 Object.setPrototypeOf(proxy, {});
代码强调了target和proxy的行为差异。Object.getPrototypeOf()给target返回的是值,而给proxy返回值时,由于getPrototypeOf陷阱被调用,返回的是null;同样,Object.setPrototypeOf()成功为target设置原型,而给proxy设置原型时,由于setPrototypeOf陷阱被调用,最终抛出一个错误。
一、 vue的$ref
一个对象,持有注册过 ref 特性 的所有 DOM 元素和组件实例。
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
<!-- `vm.$refs.p` will be the DOM node -->
<p ref="p">hello</p>
<!-- `vm.$refs.child` will be the child component instance -->
<child-component ref="child"></child-component>
ref属性不是一个标准的HTML属性,只是Vue中的一个属性。实际上,它甚至不会是DOM的一部分,所以在浏览器中你查看渲染的HTML,你是看不到有关于ref的任何东西。因为在它前面没有添加:,而且它也不是一个指令
<div ref="demo"></div>
document.querySelector('[ref=demo]');