1 代理模式的定义
代理模式:为一个对象提供一个代理用品或者占位符,以便控制对它的访问。生活中有很多这样的例子:
- 请明星代言,实际是联系明星的经纪人,来建立商户 与 明星的关系
- 二手市场买房,实际联系的是中介公司来搭建买房与买房之间的桥梁 因此,可以这样理解:当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制这个对象的访问,客户实际访问的是替身对象。替身对象对请求做出一些处理后,再讲请求转交给本体对象。
#2 代理example 我们这里使用小明送花给小美来举例子。
- 小明找到他与小美两个共同的好朋友小丑,让她帮忙送花
- 小丑不会直接送给小美,而是会找小美心情好的时候送,增加成功几率
- 小美收到鲜花接收了小明
function Flower(name) {
this.name = name;
}
// 小明使用面向对象的方式创建
var Xiaoming = function() {
}
Xiaoming.prototype.sendFlower = function(xiaochou) {
var flower = new Flower('玫瑰花');
xiaochou.recieveFlower(flower);
}
// 小丑使用javascript的对象即类的概念创建
var xiaochou = {
recieveFlower: function(flower) {
var goodDay = xiaomei.listenerGoodDay();
if (goodDay) {
xiaomei.recieveFlower(flower);
} else {
console.log('小美心情不好,因此没有送');
}
}
};
var xiaomei = {
// 小美心情随机
listenerGoodDay: function() {
var random = parseInt(Math.random() * 10);
return random % 2 === 0 ? true : false;
},
recieveFlower: function(flower) {
console.log(`小美看到是${flower.name}, 接收了小明`);
}
};
var xiaoming = new Xiaoming();
xiaoming.sendFlower(xiaochou);
复制代码
3 保护代理和虚拟代理
从上面的例子中,我们可以知道xiaochou可以帮助小美过滤掉小美不喜欢的类型:例如没有宝马车,长得矮,长得丑的,小丑可以直接拒绝。这种代理叫做保护代理。 假如鲜花价格比较昂贵,new Flower()代价很高,可以把new Flower()操作交给代理xiaochou,她可以根据xiaomei的心情好的时候再去new Flower(),避免鲜花枯萎浪费。这叫做虚拟代理。虚拟代理把一些开销很大的对象,延迟到真正需要它的时候才创建。
var xiaochou = {
recieveFlower: function() {
var goodDay = xiaomei.listenerGoodDay();
if (goodDay) {
var flower = new Flowe();
xiaomei.recieveFlower(flower);
} else {
console.log('小美心情不好,因此没有送');
}
}
};
复制代码
保护代理用于控制不同权限的对象对目标对象的访问,在javascript中并不容易实现保护代理,因为我们无法判断谁访问了某个对象。而虚拟代理是常用的一种代理模式。
4 虚拟代理加载图片
在web开发中,图片预加载是一种常用的技术。如果给某个img标签直接添加src属性,如果某个图片过大或者网速差,则图片的位置会有一段时间是空白的。常见的做法是在加载图片完毕之前使用一个loading图片。等到图片加载完毕后替换。下面我们使用虚拟代理来完成。
- 创建一个本体ImageSrc方法
- 创建一个代理ProxyImageSrc方法
// 本体
var ImageSrc = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function(src) {
imgNode.src = src;
}
};
})();
// 代理
var ProxyImageSrc = (function ProxyImageSrc() {
var img = new Image();
// 页面加载完毕后,将img上存放的src设置到ImageSrc对象上
img.onload = function() {
ImageSrc.setSrc = this.src;
};
return {
setSrc: function(src) {
// 加载中使用loading图片
ImageSrc.setSrc('http://img4.imgtn.bdimg.com/it/u=1972873509,2904368741&fm=27&gp=0.jpg');
img.src = src;
}
};
})();
// 调用,设置src
ProxyImageSrc.setSrc('http://img05.tooopen.com/images/20150820/tooopen_sy_139205349641.jpg');
复制代码
其实实现该功能,即使不引入任何模式也能够办到,例如通过下面的方式:
var ImageSrc = (function() {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
var img = new Image();
img.onload = function(src) {
imgNode = this.src;
}
return {
setSrc: function(src) {
imgNode.src = 'loading.png';
img.src = src;
}
}
})();
ImageSrc.setSrc('complete.png');
复制代码
该对象承担了多项职责。负责了图片的预加载处理,图片完成后的设置。违背了面向对象设计的原则单一职责原则
。当对象承担更多职责,意味着对象会变的巨大,引起它变化的情况会变的很多。在设计代码的时候应该高内聚低耦合,尽量符合单一职责。
5 代理和本体接口一致
当某一天我们不再需要预加载,那么就不需要设置代理对象,可以直接请求本体。其中关键点是:代理对象与本体都对外提供setSrc。在客户看来,本体与代理是一致的。代理接收的请求过程对于用户来说是透明的,用户不清楚代理与本体的区别,这样的好处是:
- 用户可以放心请求代理,他只需要关心是否能够得到预期的结果
- 在任何使用本体的地方都能够替换成为使用代理
在java语言中,代理和本体需要显示实现同一个接口,在保证它们拥有同样的方法的同时,面向接口编程迎合依赖倒置原则,通过接口向上转型,避开编译器的类型检查,使得代理与本体将来能够被替换使用。 javascript语言是动态类型语言,我们可以通过duck typing
来检查代理和本体是否拥有setSrc方法,而大多数时候不会检查。
6 虚拟代理合并http请求
下图是我们在网页中上传文件的列表,当点击checkbox则上传文件。加入手速比较快的人,1秒钟点击4,5次是不成问题的。那么相当于1秒以内,就需要与服务器进行4次请求,频繁的网络请求会带来相当大的开销。因此我们可以等待2秒以后,将2秒以内点击了的文件一起发送给服务器,如果对实时要求不是非常高的系统,2秒的延迟不会出现太大的副作用,却能大大减轻服务器的压力。
// 本体,同步文件的方法
var synchronousFile = function(ids) {
console.log(`开始同步${ids} 文件`);
}
// 代理,异步调用方法
var proxySynchronousFile = (function() {
var cacheIds = [];
var timer = null;
return function(id) {
cacheIds.push(id);
if (timer) {
return;
}
timer = setTimeout(function() {
synchronousFile(cacheIds.join(',')); // 2秒后向服务器发送请求
clearTimeout(timer);
timer = null;
cache.length = 0; // 清空集合。
},2000);
}
})();
// 注册上传文件点击事件
var checkboxs = document.getElementsByTagName('input');
checkboxs.map(function(checkbox) {
checkbox.onclick = function(event) {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
}
});
复制代码
7 虚拟代理实现缓存
缓存代理可以为一些开销大的运算结果提供暂时存储,在下次传递进来的参数如果与之前某一次的一致,直接返回存储的结果。上面存储一个通过ajax请求获取的数据。
// 本体
var getAjaxData = function(arg) {
$.ajax('url', function(data) {
return data;
});
}
// 代理
var proxyData = (function() {
var cache = [];
return function(arg) {
if (arg in cache) {
return cache[arg];
} else {
return cache[arg] = getAjaxData(arg);
}
}
})();
// 调用,第一次调用会将数据存放在cache中,第二次调用会直接使用缓存中的数据
var returnData = proxyData(1);
var retrunData2 = proxyData(1);
复制代码
8 用高阶函数创建代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。
// 本体
var mult = function() {
return [].reduce.call(arguments, function(x, y) {
return x * y;
}, 1)
}
// 本体
var plus = function() {
return [].reduce.call(arguments, function(x, y) {
return x + y;
}, 0)
}
// 代理
var calculateProxy = function(fn) {
var cache = [];
return function() {
var args = [].join.call(arguments, ',');
if (args in cache) {
return cache[args];
} else {
return cache[args] = fn.apply(this, arguments);
}
};
};
// 调用
var proxyMult = calculateProxy(mult);
var proxyPlus = calculateProxy(plus);
console.log(proxyMult(2, 4, 5));
console.log(proxyPlus(2, 4, 5));
复制代码
9 小结
代理模式包括许多小分类,在javascript中常用的是虚拟代理与缓存代理。虽然代理模式非常有用,但是我们在编写业务代码的时候,往往不需要去预先猜测是否需要使用代理模式。而是当真正发现不方便直接访问某个对象的时候,再编写代理不迟。