介绍
代理模式就是在真正使用的函数之间再加入一个函数,使得我们可以间接去访问使用的函数。那么为什么要多此一举在中间加入一个函数呢,我们可以举两个例子。
🌰一: 小明追求一位女生
方式一
- 直接把花送给她,百分百会被拒绝。
方式二
- 通过小明和女生都认识的一位中间人送花,因为中间人了解女生,成功率30%。
方式三
- 继承方式二,不过这位中间人和女生关系更加亲密,她知道女生开心的时候不会拒绝别人,所以小明把花暂存在中间人这里,中间人通过监听女生的心情来决定何时送花,成功率90%。
🌰二: 图片预加载
在平时开发中,我们可能会遇到图片过大或者网络问题导致图片加载缓慢的问题,那么我们就可以通过代理模式来实现图片的预加载。
let myImage = function () {
let imgNode = document.createElement('img')
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src
}
}
}()
let proxyImage = function () {
let img = new Image;
img.onload = function () {
myImage.setSrc(this.src)
}
return {
setSrc: function (src) {
myImage.setSrc('本地图片');
img.src = src
}
}
}()
proxyImage.setSrc("https://imgs.aixifan.com/nqiFgWPe8D-yuUNf2-YjaUfy-Mza6bm-QRF7Bj.png")
在这个例子中我们通过proxyImage间接访问了myImage。proxyImage控制了客户对myImage的访问,并且增加了一些额外的操作,比如在真正的图片加载之前,先加载一张本地的loading图片。
代理的意义
为了说明代理的意义,我们引入一个面向对象设计的原则 —— 单一职责原则。
单一职责指的是,就一个类而言,应该仅有一个引起它变化的原因。如果一个对象承担了多项职责,就意味着这个对象变得巨大,引起它变化的原因可能有多个。面向对象设计鼓励将行为分布到细粒度的对象之中,如果一个对象承担的职责过多,等于把这个职责耦合到了一起,这种耦合会导致脆弱和低内聚的设计。当变化发生时,设计可能会遭到破坏。
在面向对象的程序设计中,大多情况下,若违反其他原则,一定会违反 开放 - 封闭 原则。如果我们只是从网上获取体积很小的图片,或者5年后我们的网速已经快到可以忽略这样图片大小的限制,那么我们就可以把proxyImage方法删除掉,直接使用myImage即可。这就是代理的作用,代理负责预加载图片,操作完成之后,再把请求重新交给本体myImage。
代理和本体接口的一致性
上面说到,有一天我们不需要代理了,可以直接请求本体。其中关键是代理对象和本体都对外提供了setSrc方法,在客户看来,代理和本地是一致的,代理接手请求的过程对于用户来说是透明的,用户并不清楚代理和本体的区别,这有两个好处:
- 用户可以放心的请求代理,他只关心是否可以得到想要的结果
- 在任何使用本体的地方都可以替换成使用代理
代理模式一:虚拟代理
合并HTTP请求
有一个功能描述是点击复选框的时候,把复选框对应的文件上传到服务器,那么在客户频繁点击的时候也会同时发送大量的请求。如果不是对实时性要求特别高,我们可以创建一个请求池,到达固定的时间后统一发送请求池里的函数。
<body>
<input type="checkbox" id="1"></input>1
<input type="checkbox" id="2"></input>2
<input type="checkbox" id="3"></input>3
<input type="checkbox" id="4"></input>4
<input type="checkbox" id="5"></input>5
<input type="checkbox" id="6"></input>6
<input type="checkbox" id="7"></input>7
<input type="checkbox" id="8"></input>8
<input type="checkbox" id="9"></input>9
</body>
<script type="text/javascript">
var proxySynchronousFile = (function () {
var cache = [], // 保存一段时间内需要同步的ID
timer; // 定时器
return function (id) {
cache.push(id);
if (timer) { // 保证不会覆盖已经启动的定时器
return;
}
timer = setTimeout(function () {
synchronousFile(cache.join(',')); // 2 秒后向本体发送需要同步的ID 集合
clearTimeout(timer); // 清空定时器
timer = null;
cache.length = 0; // 清空ID 集合
}, 2000);
}
})();
var checkbox = document.getElementsByTagName('input');
//接下来,给这些checkbox 绑定点击事件,并且在点击的同时往另一台服务器同步文件:
for (var i = 0, c; c = checkbox[i++];) {
c.onclick = function () {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
}
};
</script>
代理模式二:缓存代理
计算乘积
缓存代理可以为一些开销比较大的运算结果提供暂时的存储,在下次运算时,如果传进来的参数和之前一样,则可以直接返回存储的结果。
先创建一个用于求乘积的函数:
var mult = function () {
console.log('开始计算乘积');
var a = 1;
for (var i = 0, l = arguments.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
通过增加缓存代理的方式,mult函数可以专注于自身的职责-计算乘积,缓存的功能则有代理来实现。
用高阶函数动态创建缓存代理
通过传入高阶函数这种更加灵活的方式,可以为各种计算方法创建缓存代理。
/**************** 计算乘积 *****************/
var mult = function () {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
};
/**************** 计算加和 *****************/
var plus = function () {
var a = 0;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a + arguments[i];
}
return a;
};
/**************** 创建缓存代理的工厂 *****************/
var createProxyFactory = function (fn) {
var cache = {};
return function () {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
return cache[args];
}
return cache[args] = fn.apply(this, arguments);
}
};
var proxyMult = createProxyFactory(mult),
proxyPlus = createProxyFactory(plus);
alert(proxyMult(1, 2, 3, 4)); // 输出:24
alert(proxyMult(1, 2, 3, 4)); // 输出:24
alert(proxyPlus(1, 2, 3, 4)); // 输出:10
alert(proxyPlus(1, 2, 3, 4)); // 输出:10
总结
代理模式JavaScript中最常用的就是虚拟代理和缓存代理,虽然代理模式非常有用,但我们再编写业务时,不需要去猜测是否需要使用代理模式,只有当真正发现不方便直接访问某个对象时,再编写代理也不迟。
参考
JavaScript设计模式与开发实践