看到 Proxy就应该想到代理模式(Proxy Pattern),Proxy
是 Javascript ES2015 标准的一部分,我们应该学会使用它,代理模式是一种设计模式,使用 Proxy
对象可以轻而易举的在 Javascript 中创建代理模式。然而,使用设计模式并不是目的,目的在于解决实际问题。本文首先会简单介绍 Proxy
的基本用法,接着将会叙述如何使用 Proxy
创建代理模式并且对我们的应用进行优化。
Proxy 的基本使用
开始学习 Proxy
的使用之前,建议首先对 Reflect 有一定的了解,如果很陌生的话,建议先花 1 分钟浏览相关知识。
好了,现在假设已经具备了一定的 Reflect
知识,就开始掌握 Proxy
吧。
基本语法
和 Proxy
相关的方法一共就两个:
- 构造方法 本文着重讨论
Proxy.revocable()
创建一个可撤销的Proxy
对象,其余与构造函数类似,理解了Proxy
的构造方法后,该方法与构造方法使用非常类似,本文不再涉及
接下来本文将围绕 Proxy
的构造方法进行讲解。
let p = new Proxy(target, handler);
复制代码
参数
-
target 任何类型的对象,包括原生数组,函数,甚至另一个
Proxy
对象 -
handler 一个对象,其属性是当执行一个操作时定义代理的行为的函数, 允许的属性一共 13 种,与
Reflect
的方法名一致
返回
- p
Proxy
对象
注意:
new Proxy
是稳定操作,不会对target
有任何影响。
下面来看几个代表性的例子,便于加深理解。
代理一个对象字面量:
const target = {};
const handler = {
set: (obj, prop, value) => {
obj[prop] = 2 * value;
},
get: (obj, prop) => {
return obj[prop] * 2;
}
};
const p = new Proxy(target, handler);
p.x = 1; // 使用了 set 方法
console.log(p.x); // 4, 使用了 get 方法
复制代码
代理一个数组:
const p = new Proxy(
['Adela', 'Melyna', 'Lesley'],
{
get: (obj, prop) => {
if (prop === 'length') return `Length is ${obj[prop]}.`;
return `Hello, ${obj[prop]}!`;
}
}
);
console.log(p.length) // Length is 3.
console.log(p[0]); // Hello, Adela
console.log(p[1]); // Hello, Melyna
console.log(p[2]); // Hello, Lesley
复制代码
代理一个普通函数:
const foo = (a, b, c) => {
return a + b + c;
}
const pFoo = new Proxy(foo, {
apply: (target, that, args) => {
const grow = args.map(x => x * 2);
const inter = Reflect.apply(target, that, grow);
return inter * 3;
}
});
pFoo(1, 2, 3); // 36, (1 * 2 + 2 * 2 + 3 * 2) * 3
复制代码
代理构造函数
class Bar {
constructor(x) {
this.x = x;
}
say() {
console.log(`Hello, x = ${this.x}`);
}
}
const PBar = new Proxy(Bar, {
construct: (target, args) => {
const obj = new Bar(args[0] * 2);
return obj;
}
});
const p = new PBar(1);
p.say(); // Hello, x = 2
复制代码
Proxy
的基本用法无出其上,可 Proxy
的真正用途还没有显现出来,接下来结合设计模式中的一种模式 —— 代理模式 —— 进一步讨论。
使用 Proxy 创建代理模式
从上面的例子并不能看出 Proxy
给我们带来了什么便利,需要实现的功能完全可以在原函数内部进行实现。既然如此,使用代理模式的意义是什么呢?
- 遵循“单一职责原则”,面向对象设计中鼓励将不同的职责分布到细粒度的对象中,
Proxy
在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念 - 遵循“开放-封闭原则”,代理可以随时从程序中去掉,而不用对其他部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种原因不再需要代理,那么就可以容易的将代理对象换成原对象的调用
达到上述两个原则有一个前提就是代理必须符合“代理和本体接口一致性”原则:代理和原对象的输入和输出必须是一致的。这样对于用户来说,代理就是透明的,代理和原对象在不改动其他代码的条件下是可以被相互替换的。
代理模式的用途很广泛,这里我们看一个缓存代理的例子。
首先创建一个 Proxy
的包装函数,该函数接受需要创建代理的目标函数为第一个参数,以缓存的初值为第二个参数:
const createCacheProxy = (fn, cache = new Map()) => {
return new Proxy(fn, {
apply(target, context, args) {
const argsProp = args.join(' ');
if (cache.has(argsProp)) {
console.log('Using old data...');
return cache.get(argsProp);
}
const result = fn(...args);
cache.set(argsProp, result);
return result;
}
});
};
复制代码
然后我们使用乘法函数 mult
去创建代理并调用:
const mult = (...args) => args.reduce((a, b) => a * b);
const multProxy = createCacheProxy(mult);
multProxy(2, 3, 4); // 24
multProxy(2, 3, 4); // 24, 输出 Using old data
复制代码
也可以使用其他的函数:
const squareAddtion = (...args) => args.reduce((a, b) => a + b ** 2, 0);
const squareAddtionProxy = createCacheProxy(squareAddtion);
squareAddtionProxy(2, 3, 4); // 29
squareAddtionProxy(2, 3, 4); // 29, 输出 Using old data
复制代码
对于上面这个例子,有三点需要注意:
- 对于检测是否存在旧值的过程较为粗暴,实际应用中应考虑是否应该使用更为复杂精确的判断方法,需要结合实际进行权衡;
createCacheProxy
中的console.log
违背了前文所说的“代理和本体接口一致性”原则,只是为了开发环境更加方便性的调试,生产环境中必须去掉;multProxy
与squareAdditionProxy
是为了演示使用方法而在这里使用了相对简单的算法和小数据量,但在实际应用中数据量越大、fn
的计算过程越复杂,优化效果越好,否则,优化效果不仅有可能不明显反而会造成性能下降
代理模式的实际应用
这一节结合几个具体的例子来加深对代理模式的理解。
函数节流
如果想要控制函数调用的频率,可以使用代理进行控制:
需要实现的基本功能:
const handler = () => console.log('Do something...');
document.addEventListener('click', handler);
复制代码
接下来使用 Proxy
进行节流。
首先使用构造创建代理函数:
const createThrottleProxy = (fn, rate) => {
let lastClick = Date.now() - rate;
return new Proxy(fn, {
apply(target, context, args) {
if (Date.now() - lastClick >= rate) {
fn(args);
lastClick = Date.now();
}
}
});
};
复制代码
然后只需要将原有的事件处理函数进行一曾包装即可:
const handler = () => console.log('Do something...');
const handlerProxy = createThrottleProxy(handler, 1000);
document.addEventListener('click', handlerProxy);
复制代码
在生产环境中已有多种工具库实现该功能,不需要我们自己编写
图片懒加载
某些时候需要延迟加载图片,尤其要考虑网络环境恶劣以及比较重视流量的情况。这个时候可以使用一个虚拟代理进行延迟加载。
首先是我们最原始的代码:
const img = new Image();
img.src = '/some/big/size/image.jpg';
document.body.appendChild(img);
复制代码
为了实现懒加载,创建虚拟图片节点 virtualImg
并构造创建代理函数:
const createImgProxy = (img, loadingImg, realImg) => {
let hasLoaded = false;
const virtualImg = new Image();
virtualImg.src = realImg;
virtualImg.onload = () => {
Reflect.set(img, 'src', realImg);
hasLoaded = true;
}
return new Proxy(img, {
get(obj, prop) {
if (prop === 'src' && !hasLoaded) {
return loadingImg;
}
return obj[prop];
}
});
};
复制代码
最后是将原始的图片节点替换为代理图片进行调用:
const img = new Image();
const imgProxy = createImgProxy(img, '/loading.gif', '/some/big/size/img.jpg');
document.body.appendChild(imgProxy);
复制代码
异步队列
这个需求是很常见的:前一个异步操作结束后再进行下一个异步操作。这部分我使用 Promise
进行实现。
首先构造一个最为简单的异步操作 asyncFunc
:
const callback = () => console.log('Do something...');
const asyncFunc = (cb) => {
setTimeout(cb, 1000);
}
asyncFunc(callback);
asyncFunc(callback);
asyncFunc(callback);
复制代码
可以看到控制台的输出是 1s 之后,几乎是同时输出三个结果:
// .. 1s later ..
Do something...
Do something...
Do something...
复制代码
接下来我们使用 Promise
实现异步队列:
const createAsyncQueueProxy = (asyncFunc) => {
let promise = null;
return new Proxy(asyncFunc, {
apply(target, context, [cb, ...args]) {
promise = Promise
.resolve(promise)
.then(() => new Promise(resolve => {
Reflect.apply(asyncFunc, this, [() => {
cb();
resolve();
}, ...args]);
}));
}
});
};
复制代码
上面这段代码通过 Promise
实现了异步函数队列,建议在理解了 Promise
之后再理解阅读上面这段代码。
上面这段代码测试通过,有两点需要注意:
promise
的值并不能确定是否为Promise
,需要使用Promise.resolve
方法之后才能使用then
方法Reflect.apply
方法中的第三个参数是数组,形同与Function.prototype.apply
的第二个参数
然后使用代理进行替换并调用:
const timeoutProxy = createAsyncQueueProxy(asynFunc);
timeoutProxy(callback);
timeoutProxy(callback);
timeoutProxy(callback);
复制代码
可以看到控制台的输出已经像我们期望的那样: 前一个异步操作执行完毕之后才会进行下一个异步操作。
// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
// .. 1s later ..
Do something...
复制代码
除了上面这种使用代理的方式实现异步队列外,在我的另一篇博客进阶 Javascript 生成器中,还使用了另外一种方式。
结语
本文首先介绍了 ES2015 中关于 Proxy
的基本用法,接着讨论了代理模式的使用特点,然后结合实际列举了几种常见的使用场景。最后列举一些比较有价值的参考资料供感兴趣的开发者继续阅读。