响应式数据的未来——Proxy
最近Vue发布了在即将到来的新版本产生的变化,其中提到的很重要的一点就是将用Proxy
代替Object.defineProperty
实现数据的响应式。正巧mobx
从4升级到5的过程中也用Proxy
重写了自己的响应式系统,可以看到这个ES6特性确实称得上是响应式实现首选,好处甚至大过对兼容性的考虑。过去由于兼容性原因没有深入使用这个特性的机会,现在看来有必要再好好了解一下。
定义概览
Proxy
这个概念其实并不陌生,是在各种应用场景中都会接触到的一种模式。比如设计模式中的代理模式,计算机网络中的正向代理,反向代理等等,从原理上来说都指的是同一件事,就是当A对某个目标Target进行操作时,在两者间创建一个ProxyTarget对Target进行虚拟,ProxyTarget可以拦截原操作,并在Target原操作的基础上增加一些新的操作,定义为一个新的响应原操作的函数。而对于A来说,新增的这个ProxyTarget对其是不可见的,所以对于这个新的响应原操作的函数有一个非常形象的学名——trap(陷阱)。
那么又有一个新的问题来了,既然陷阱函数对A来说和原函数没有区别,陷阱函数就必然能进行和原函数完全相同的操作,让A发现不了这个陷阱函数额外多做的一些事。为了方便陷阱函数调用原始操作,就将能被trap的原操作统一集合到名为Reflect
的反射对象的静态方法上。
以上就是使用这个特性需要了解的全部概念,其实还是很好理解的,毕竟代理模式并不是一个新概念,也是常规开发中用的相对较多的一种设计模式。下表列出了Proxy
能够trap的所有操作,原操作也能在Reflect
上找到完全的对应。
代理陷阱 | 重写行为描述 |
---|---|
get |
读取一个属性值 |
set |
设置一个属性值 |
has |
in 运算符操作 |
deleteProperty |
delete 运算符操作 |
getPrototypeOf |
Object.getPrototypeOf() |
setPrototypeOf |
Object.setPrototypeOf() |
isExtensible |
Object.isExtensible() |
preventExtensions |
Object.preventExtensions() |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor() |
defineProperty |
Object.defineProperty() |
ownKeys |
Object.keys() Object.getOwnPropertyNames() Object.getOwnPropertySymbols() |
apply |
函数调用 |
construct |
使用new 调用函数 |
常规应用实例
对象代理
get
该陷阱函数会在读取属性时被调用,那么就可以针对这个被读取的属性做一些事,比如说验证该属性是否存在。其参数为如下三个:
trapTarget
:被代理的目标对象key
: 被读取的键名receiver
:代理对象本身
const proxy = new Proxy({
name: 'a'
}, {
get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError(`no ${key}`);
}
return Reflect.get(trapTarget, key, receiver); // 原操作
}
});
console.log(proxy.name) // 'a'
console.log(proxy.a) // 'no a'
set
同样的,get
可以针对获取的属性做一些验证,当设置属性时,我们也可以通过set
陷阱函数对被设置的属性做验证, 比如说限制设置属性的类型。
相比于get
的参数类型,除了同样拥有trapTarget
,key
,receiver
外,还增加了一个当前被设置的属性值value
。
const proxy = new Proxy({
name: 'a'
}, {
set(trapTarget, key, value, receiver) {
if (!trapTarget.hasOwnProperty(key) && isNaN(value)) {
throw new TypeError('Added property must be a number');
}
return Reflect.set(trapTarget, key, value, receiver); // 原操作
}
});
proxy.name = 'b' // 'ok'
proxy.a = 'b' // 'Added property must be a number'
ownKeys
该陷阱函数的默认行为会返回由全部自有属性的键组成的数组,比较常见的应用方式就是根据需求过滤出不想被枚举的属性,比如以_
开头的各种私有属性和方法。其接受参数只有一个trapTarget
,必须返回一个数组或者类数组对象。
const proxy = new Proxy({
name: 'a',
_name: 'b'
}, {
ownKeys(trapTarget) {
return Reflect.ownKeys(trapTarget).filter(key => key[0] !== '_');
}
});
console.log(Object.keys(proxy)) // ["name"]
函数代理
代理陷阱中有两个针对于函数目标——apply
和construct
,分别对应于函数的直接执行和构造执行。这两种形式基本上覆盖了所有场合的函数调用,换句话说,通过函数代理我们可以完全控制任何函数的常规行为,给予了我们相当大的可操作空间。
参数和调用方式验证
最常见的应用场景还是各种调用的验证了,比如限定某个函数不能用构造函数的形式执行,或者参数类型的限制。
function example (...args) {
// ...
}
const exampleProxy = new Proxy(example, {
apply(trapTarget, thisArg, argumentList) {
if (argumentList.some(item => typeof item !== 'number')) {
throw new TypeError('args must be numbers'); // 限制参数类型为number
}
return Reflect.apply(trapTarget, thisArg, argumentList);
}
construct(trapTarget, argumentList) {
throw new TypeError("can't be called with new"); // 限制构造函数调用
}
});
console.log(exampleProxy('a')) // error
console.log(new exampleProxy()) // error
缓存代理
对一些开销大的函数每次调用时,可以考虑将重复的运算结果缓存提升性能。
function example (...args) {
// ...
}
const cacheExampleProxy = (function() {
const cache = new Map();
return new Proxy(example, {
apple(trapTarget, thisArg, argumentList) {
const hash = getHash(...args);
if (cache.has(hash)) {
return cache.get(hash);
}
const result = Reflect.apply(trapTarget, thisArg, argumentList);
cache.set(hash, result);
return result;
}
});
})();
小结
以上也只是列出了Proxy
的应用方式的一小部分,不过也足以说明这个特性的使用方法了。陷阱函数虽然有十几种之多,但可以总结为针对对象和函数的底层操作行为一些hook,如果有什么业务或者需求是适合在对“对象的处理或者函数的调用”这个切面去用的,就可以考虑用Proxy
封装一些通用实现,当然也不要忘了兼容性的问题。
-- EOF --