当你想买商业保险的时候,却不得不亲自去了解不同公司的方案和限制。在自己百忙之中分神和不同的销售博弈。有了职业的保险经纪人,你只要向他/她讲述你的需求,经纪人就用自己的关系量身定做了一套方案给你。买哪家,怎么买,等等。保险经纪代理的模式,很好地体现了程序设计关注点分离的思想。
代理作为一种工作模式,代理也是非常重要。多人协作开发中,开发不应当直面具体的测试人员。而应该有项目经理之类的人梳理bug需求,根据项目计划区分优先级。然后程序员面向梳理后的bug保质保量完成目标,这样程序员就只顾修bug,而测试无需拿着bug去追着程序,出了问题先要排查前后端,然后还要排查环境问题,迟迟几天没有结果——那本不是测试该干的活,况且毫无意义。
老电影《大鼻子情圣》有这样一个场景:A暗恋C,但他俩并不认识。刚好两人有一个共有的朋友B,于是A请求B帮忙送一束花给C。
用代码来表示就是:
const a={
sendFlower(target){
const flower=new Flower();
target.receiveFlower(flower);
},
price:1000
}
const b={
receiveFlower(flower){
c.receiveFlower(flower);
}
}
const c={
receiveFlower(flower){
console.log('收到fa')
}
}
a.sendFlower(b)
b是个靠谱的代理,所以a送花给b等同于送花给c。
然而这种代码还是没什么用,接下来尝试让场景变得复杂些。假如B是个情商高,她会从送花的价格判断这个人有木有钱,穷逼就不要打扰了。另一方面,B还会挑C心情好的时候送花,因此需要监听C心情的变化,假设C在3秒之后心情会变好:
const b={
receiveFlower(flower){
c.listenGoodMood(()=>{
if(a.price>999){
c.receiveFlower(flower);
}
return false;
})
}
}
const c={
receiveFlower(flower){
console.log('收到fa')
},
listenGoodMood(fn){
setTimeout(()=>{
fn();
},3000);
}
}
从此可以看到代理的作用,B可以帮C过滤掉一些不好的人选,比如没钱的。B充当了黑脸的模式,而C是为了保持完美形象,不希望直接拒绝任何人。这样的模式称为保护代理。
另一方面,从现实过程来看,上述的代理还存在问题。花是有保质期的(new FLower的代价高昂),如果C心情好的时候,再买花,就是最合适了。——这就是虚拟代理。它总是把一些开销很大的事情,放到需要用到的时候才执行。
图片异步加载是一个非常常用的技术。非常适合使用代理模式。其实现机制是:创建一个普通的本体对象,负责往页面添加img标签,并提供一个setSrc方法:
const createImg=(function(){
const img=document.createElelment('img');
document.body.appendChild(img);
return setSrc:(src)=>{
img.src=src
}
})();
createImg=setSrc('xxx.jpg');
当网速很慢时,这张图可能会加载很长的时间,此时考虑给加载中的图片设置一张loading图。
const proxyImg=(function(){
const img=new Image();
img.onload=function(){
createImg.setSrc(this.src);
};
return {
setSrc:function(src){
createImg.setSrc('loading.gif');
img.src=src;
}
}
})();
proxyImg.setSrc('xxx.jpg');
通过引入代理,把预加载的事给做了。
代理模式也完全可以在一个方法里实现。但是需要注意的是这违反了单一职责原则.。
前文提过,单一职责原则指的是,就一个类(通常也包括对象和函数等)而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象将变得巨大,引起它变化的原因可能会有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这些职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到意外的破坏。
如果引入代理,当任意需求变动后,单一职责原则就会很有用。在图片异步加载的案例中,倘若哪天网速变快,那就不再需要代理模式了。只需要把方法替换为原生的createImg即可。完全无痛。
web世界里,最大的开销不是CPU,而是网络请求。设想,我有一个按钮点击一次就发送一次请求,那么不排除有几十年单身的玩家,在一秒钟连续点击按钮10次。这意味着连续触发了10次请求。因此节流就很重要了。
// 节流
const throttle=(fn,interval)=>{
const lastTime=0;
return function(...args){
let now=new Date();
if(now-lastTime>interval){
fn.apply(this,args);
lastTime=now;
}
}
}
const todo=()=>{
// ...
}
btn.addEventListener('click',function(e){
throttle(todo,2000)
})
原书作者写过一个miniConsole框架,用户也不一定每个页面都需要用,我们希望在使用的时候才开始加载它。于是定义为按下f2时。在代码里,如果遇到执行miniConsole的代码,会被通通接受放到缓存(cache)里。一旦真正的miniConsole开始调用,就会以命令队列的形式去真正执行它:
let miniConsole={
log:function(){
window.cache.push(function(){
return miniConsole.log.apply(miniCOnsole,arguments)
})
}
}
const handler = function (ev) {
if (ev.keyCode === 113) {
var script = document.createElement('script');
script.onload = function () {
for (let i = 0, fn; fn = window.cache[i++];) {
fn();
}
};
script.src = 'miniConsole.js';
document.getElementsByTagName('head')[0].appendChild(script);
}
};
document.body.addEventListener('keydown',handler);
我曾经问过来面试前端这样一个问题。
一个web日历应用,点击请求付费接口获取黄历信息。如果用户有事没事点击,势必造成很大的开销。这时有什么解决策略?
其实这就是缓存代理的问题。考虑中转服务器代理请求,如果没有请求过,则由中转服务器请求,并且放到本地缓存中,如果有这天的数据,就从缓存中发回。前端一样是有一个代理,缓存已经发送过的请求结果。
这是笔者工作中遇到的另外一个场景:一个表格,包含所有的资金累计,由于原生js的不准确性,又不能映入其它的资金计算框架。只能调用后端接口进行计算。而在表格操作后,要求经常刷新表格——而统计对此表格并没有任何关联,却反复计算,浪费了资源。
这个时候就很适合使用缓存代理。我们尝试稍微抽象这个工作问题,用一个对象来缓存计算结果,只要参数一样,就返回一致的结果。
/**
* 此处模仿计算量大的计算
*/
const calc=function(a,b){
console.log('计算了')
return a+b;
}
const proxyCalc = (function() {
let cache = {};
return function(){
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}else{
return cache[args] = calc.apply(this, arguments);
}
}
})();
proxyCalc(1,2) // 3
proxyCalc(1,2) // 3
你会发现,两次都返回了一致的结果,但是计算只有一次。
现在把原来的场景带上,如果calc是个异步计算呢?操作差不多我们无法直接把计算结果放到代理对象的缓存中,而是要通过promise的方式。具体代码如下:
const calc=function(a,b){
console.log('计算了');
// 模拟一个耗时的异步请求
setTimeout(()=>{
return a+b;
});
}
const proxyCalc = (function() {
let cache = {};
return async function(){
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}else{
cache[args] = await calc.apply(this, arguments);
}
return cache[args];
}
})();
proxyCalc(1,2)
setTimeout(()=>{
proxyCalc(1,2)
})
也许此时已经有很多读者燥起来了。 proxyCalc
适用性太低了。可用的一个代理,应该是处理一类相似的计算方法,而不是一个。因此需要改写 proxyCalc
。
那么就考虑创建代理工厂:
var createProxyFactory=function(fn){
varcache={};
return function(){
var args=Array.prototype.join.call(arguments,',');
if(args in cache){
return cache[args];
}
return cache[args]=fn.apply(this,arguments);
}
};
你就可以用 createProxyFactory
处理任意的数字计算了。