Proxy 代理
讲讲API
const p = new Proxy(target, handler)
target: 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为。
Proxy 常用用法
var handler = {
//读取
get: function (target, name, receiver) {
if (name === 'prototype') {
return Object.prototype;
}
return 'Hello, ' + name;
},
//设置
set (target, propKey, value, receiver) {
if (propKey.includes('_')) {
throw new TypeError('it is private');
}
obj[prop] = value;
return true;
},
//拦截函数调用
apply: function (target, thisBinding, args) {
return args[0];
},
//拦截new
construct: function (target, args) {
return { value: args[1] };
},
//拦截HasProperty操作
has: function (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
//其他拦截如delete,setPrototypeOf, setPrototypeOf等都是一些api的拦截
};
var proxyObj = new Proxy(function (x, y) {
return x + y;
}, handler);
proxyObj(1, 2) // 拦截函数,默认返回第一项 1
new proxyObj(1, 2) // 拦截构造函数 {value: 2}
proxyObj.prototype === Object.prototype //改造原型对象 true
proxyObj.foo === "Hello, foo" //读取操作默认浅比较 true
var privateObj = new Proxy({
_context: { data: 'data' }
}, handler);
privateObj._context = { data: null } // _ 私有属性不能设置
if (data in privateObj._context) { //has拦截
console.log('exist _context');
}
Proxy 简单来解释就一个拦截器,你可以监听任意对象的属性修改,在修改前你可以实现你自己的逻辑,那么你也就可以靠自己实现如:
- 禁止不合法的属性名修改
- 实现各种数据类型的原型对象的方法重写
- 用于数据的映射转换、常见的如前端的接口返回数据转换、ORM操作数据库
Proxy 的this问题
在proxy中的this不是目标对象的透明代理,无法保证与目标对象一致, 目标对象内部的this关键字会指向 Proxy 代理, 而不是Proxy本身,这意味着proxy内部的this是不受控制的,代理的对象随时都可能被其他拦截操作而改变,影响实际结果。而Reflect就能很好的解决this问题。
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {}); //指向的是_name
proxy.name // 此时访问proxy,但是this已经指向了_name, 所以undefined
Reflect 反射
Reflect的api,跟proxy都能对应上,要说复刻都不为过
Reflect.apply(target, thisArg, args)
Reflect.construct(target, args)
Reflect.get(target, name, receiver)
Reflect.set(target, name, value, receiver)
Reflect.defineProperty(target, name, desc) //老版本的拦截
Reflect.deleteProperty(target, name) //delete 关键字
Reflect.has(target, name) //in关键字
Reflect.ownKeys(target) //返回对象的所有属性
Reflect.isExtensible(target) //对象是否可扩展
Reflect.preventExtensions(target) //操作让对象不可扩展,返回是否已修改的结果
Reflect.getOwnPropertyDescriptor(target, name) //获取原型
Reflect.getPrototypeOf(target) //获取原型
Reflect.setPrototypeOf(target, prototype) //设置原型
通过观察者模式的简单实现来引出 Reflect 的用法
//观察者模式
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, { set });
//vue响应式系统的底层增删改查的属性的改变都是通过Proxy拦截和Reflect操作对象,每个响应式的deps的map集合,deps有对应的不同的副作用函数effectFn的set集合,然后做遍历执行。
function set (target, key, value, receiver) {
//receiver 处理this始终执行proxy,而不是target
const result = Reflect.set(target, key, value, receiver);
queuedObservers.forEach(observer => observer());
return result;
}
//被观察目标对象
const person = observable({
name: '张三',
age: 20
});
function print () {
console.log(`${person.name}, ${person.age}`)
}
//观察person
observe(print);
person.name = '李四';
上面的例子是要person.name改变,那么Proxy的代理后,就会触发set,那么我们之前用Proxy的时候set内部其实用了对象内的原型方法,在这个例子我们却用了Reflect,本质来说我们用Reflect模拟Object的一些api操作,包括this的解决。还有一个需要注意的是,proxy的拦截的api跟reflect拦截api都是一一对应的。所以你可以简单理解在每个proxy拦截操作时,你都需要用Reflect来处理对象属性操作的逻辑。
// proxy的set拦截老的判断
'assign' in Object // true
// proxy的set拦截新的判断
Reflect.has(Object, 'assign') // true
从上面的代码能明显感觉到相比Object的各种表达式,Reflect的函数式写法代码可读性其实更好。当然和Object操作还有其他一些细微的差别....
// 在无法定义属性时,会抛出一个错误, 多余的try catch !
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法默认返回false, 是不是更好些?
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
总结
Reflect可以看做解决了Proxy的this指向问题,同时更好的兼容和代码可读性成为了Proxy拦截操作对象的绝佳搭档。Proxy解决了defineProperty原来的监听的局限性带来的性能缺陷。同时更多的拦截api的优势,让vue框架底层使用Proxy、Reflect来实现响应式系统。