参考资料
曾探《JavaScript设计模式与开发实践》;
定义
为一个对象提供一个代用品或占位符,以便控制对它的访问。代理模式的关键是当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问。客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本体对象。
使用场景:
图片预加载;
缓存数据;
本地开发设置代理,解决跨域问题;
虽然代理模式非常有用,但我们在编写业务代码的时候,往往不需要预先猜测是否需要使用代理模式。当真正发现不方便直接访问某个对象的时候,再编写代理也不迟。
代理的意义
单一职责原则:一个类(通常包括对象和函数等)而言,应该仅有一个引起它变化的原因。
保护代理和虚拟代理
代理B可以帮助A过滤掉一些请求,比如送花的人中年龄太大的或者没有宝马的,这种请求就可以直接在代理B处被拒绝掉,这种代理叫做保护代理。假设现实中花的价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时在执行new Flower,这种代理叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才去创建。保护代理用于控制不同权限的对象对目标对象的访问。保护代理用于控制不同权限的对象对目标对象的访问,而虚拟代理是最常用的一种代理模式。
虚拟代理实现图片预加载
var myImage = (function(){
var imgNode = document.createElement( 'img' );
document.body.appendChild( imgNode );
return {
setSrc: function( src ){
imgNode.src = src;
}
}
})();
var proxyImage = (function(){
var img = new Image;
img.onload = function(){
myImage.setSrc( this.src );
}
return {
setSrc: function( src ){
myImage.setSrc( 'file:// /C:/Users/svenzeng/Desktop/loading.gif' );
img.src = src;
}
}
})();
proxyImage.setSrc( 'http:// imgcache.qq.com/music/photo/k/000GGDys0yA0Nk.jpg' );
虚拟代理合并HTTP请求
假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外一台服务器上面,此时,我们的思路就是当checkbox被选中时,把选中的文件传到另一台服务器。代码如下:
var synchronousFile = function (id) {
console.log('开始同步文件', id)
}
var checkbox = document.getElementsByTagName('input')
for (var i = 0, c; c = checkbox[i++]) {
c.onclick = function() {
if (this.checked === true) {
synchronousFile(this.id);
}
}
}
虽然功能实现了,但是会存在很多问题:当我们连续快速点击时,会发送很多个请求,这会带来相当大的开销。我们的解决方案是,通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发给服务器,如果不是实时性要求很高的系统,有一点延迟并不会带来太大的副作用,却能大大减轻服务器的压力。代码如下:
var synchronousFile = function (id) {
console.log('开始同步文件', id)
}
var proxySynchronousFile = function() {
var cache = [],
timer;
return function(id) {
cache.push(id);
if (timer) {
return;
}
timer = settimeout(() => {
synchronousFile(cache.join(','));
clearTimeout(timer);
timer = null;
cache.length = 0; // 清空id集合
}, 2000)
}
}()
var checkbox = document.getElementsByTagName('input')
for (var i = 0, c; c = checkbox[i++]) {
c.onclick = function() {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
}
}
缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面的存储的运算结果。这里引用书中缓存代理的例子-计算乘积来理解一下缓存代理的功能:
var mult = function() {
var a = 1;
for (var i = 0, l = argumments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
}
mult(2, 3) // 6
mult(2, 3, 4) // 24
加入缓存代理函数:
var proxyMult = (function() {
var cache = {};
return function() {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
return cache[args] = mult.apply(this, arguments);
}
})()
proxyMult(1, 2, 3, 4) // 24
proxyMult(1, 2, 3, 4) // 24
当第二次调用proxyMult(1, 2, 3, 4)时,本体mult函数并没有被计算,proxyMult直接返回了之前计算好的结果。通过增加缓存代理的方式,mult函数可以继续专注于自身的职责——计算乘积,缓存功能是由代理对象实现的。
缓存分页数据
分页场景在平时的开发中是非常常见的了,通常我们的做法是,每次切换分页时,都重新请求一次接口,如果对于一些不会经常变动的列表来说,每次重新请求就没有必要了,此时我们可以类比前面的缓存代理-计算乘积的例子,缓存一下分页数据:
const getList = function(page) {
return axios.get('/api/list', { page })
},
const proxyGetList = function() {
const cache = {}
return async function(page) {
if (cache[page]) {
return cache[page]
}
const list = await getList(page)
cache[page] = list
return list
}
}
这种方式虽然可以减少不少的接口请求,但由于使用缓存数据,容易导致数据不能及时的更新,所以在实际的开发中,我并没有使用这种方式优化。而对于一些项目配置信息,例如用户信息等,我们完全可以通过vuex进行保存数据,当然,如果明确列表数据不会发生变化也可以考虑使用代理模式缓存下不同页数的数据,还是根据具体的场景来决定了。
其他的代理模式
防火墙代理:控制网络资源的访问,保护”主题“不让坏人接近;
远程代理:为一个对象在不同的地址空间提供局部代表,在Java中,远程代理可以是另一个虚拟机中的对象;
智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数;
写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是典型的应用场景。