Life Will Change Lyn
  1. 1 Life Will Change Lyn
  2. 2 Time Bomb Veela
  3. 3 Quiet Storm Lyn
  4. 4 Last Surprise Lyn
  5. 5 Libertus Chen-U
  6. 6 Hypocrite Nush
  7. 7 Warcry mpi
  8. 8 Flower Of Life 发热巫女
  9. 9 The Night We Stood Lyn
  10. 10 One Last You Jen Bird
  11. 11 かかってこいよ NakamuraEmi
2018-11-25 17:25:28

响应式数据的未来——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,keyreceiver外,还增加了一个当前被设置的属性值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"]

函数代理

代理陷阱中有两个针对于函数目标——applyconstruct,分别对应于函数的直接执行和构造执行。这两种形式基本上覆盖了所有场合的函数调用,换句话说,通过函数代理我们可以完全控制任何函数的常规行为,给予了我们相当大的可操作空间。

参数和调用方式验证

最常见的应用场景还是各种调用的验证了,比如限定某个函数不能用构造函数的形式执行,或者参数类型的限制。

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 --

添加在分类「 前端开发 」下,并被添加 「JavaScript」「设计模式」 标签。