代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问。
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象,替身对象对请求做出一些处理之后,再把请求转交给本体对象。
举个例子,A与B是好朋友,但是有一天两人吵架了,于是A决定向B发消息道歉,这段行为用代码简单描述一下,如下所示:
var Message = function () {};
var A = {
// 发送消息方法
sendMessage: function (target) {
var message = new Message();
target.receiveMessage(message);
},
};
var B = {
// 接收消息方法
receiveMessage: function (message) {
console.log("收到消息:" + message);
},
};
A.sendMessage(B);
但是,如果B拉黑了A,导致A无法通过发消息给B,只能通过两人的共同好友C来表达,这时A通过C来向B道歉,如下所示:
var Message = function () {};
var A = {
// 发送消息方法
sendMessage: function (target) {
var message = new Message();
target.receiveMessage(message);
},
};
var C = {
// C接收消息,并发送给B
receiveMessage: function (message) {
B.receiveMessage(message);
},
};
var B = {
// 接收消息方法
receiveMessage: function (message) {
console.log("收到消息:" + message);
},
};
A.sendMessage(C);
如果C很了解B,B心情好的时候会与A和解,B心情不好的时候调解会失败,那么C可以通过监听B的心情变化来决定什么时候发消息给B,代码如下:
var Message = function () {};
var A = {
// 发送消息方法
sendMessage: function (target) {
var message = new Message();
target.receiveMessage(message);
},
};
var C = {
// 接收消息,并发送给B
receiveMessage: function (message) {
// 监听B的好心情,在心情好时发消息
B.listenGoodMood(function () {
B.receiveMessage(message);
});
},
};
var B = {
// 接收消息方法
receiveMessage: function (message) {
console.log("收到消息:" + message);
},
// 监听心情变化方法
listenGoodMood: function (fun) {
// 假设1s之后心情变好
setTimeout(() => {
fun();
}, 1000);
},
};
A.sendMessage(C);
在上述例子中,代理C可以帮助B过滤掉一些消息,比如B不认识的人,或者B讨厌的人,这些消息在代理C中被过滤掉,这叫做保护代理。
保护代理用于控制不同权限的对象对目标对象的访问,但在JavaScript
并不容易实现保护代理,因为我们无法判断谁访问了某个对象。
C可以选择在B心情好的时候发送消息,使成功概率增加,这叫做虚拟代理。虚拟代理可以把一些开销很大的对象,延迟到真正需要它的时候才去创建。例如:
var C = {
// 接收消息,并发送给B
receiveMessage: function () {
// 监听B的好心情,在心情好时发消息
B.listenGoodMood(function () {
var message = new Message();
B.receiveMessage(message);
});
},
};
Web
开发中,图片预加载是一种常用的技术,如果直接给某个img
标签节点设置src
属性,如果图片过大或者网络不佳,图片的位置往往有段时间会是一片空白。
常见的做法是先用一张loading
图片占位,然后用异步的方式加载图片,等图片加载好了再把它填充到img
节点里,这种场景就很适合使用虚拟代理。
下面我们来实现这个虚拟代理,首先创建一个普通的本体对象,这个对象负责往页面中创建一个img
标签,并且提供一个对外的setSrc
接口,外界调用这个接口,便可以给该img
标签设置src
属性:
var myImage = (function () {
// 创建img标签
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
// 设置图片src方法
imgNode.src = src;
},
};
})();
myImage.setSrc("XXXXXXX");
如果我们把网速调慢,然后通过MyImage.setSrc
给该img
节点设置src
,可以看到,在图片被加载好之前,页面中有一段长长的空白时间。
现在开始引入代理对象proxyImage
,通过这个代理对象,在图片被真正加载好之前,页面中将出现一张占位的loading.gif
, 来提示用户图片正在加载。代码如下:
var myImage = (function () {
// 创建img标签
var imgNode = document.createElement("img");
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
// 设置图片src方法
imgNode.src = src;
},
};
})();
// 代理对象,在图片加载好之前,放一张loading图片
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
};
return {
setSrc: function (src) {
myImage.setSrc("XXXXXXXXXX");
img.src = src;
},
};
})();
proxyImage.setSrc("XXXXXXXXXX");
例如以下场景,上学时每天都需要写作业,作业最后交给老师批阅,但是老师可能带了许多个班级,每个班级又有很多学生,如果老师亲自收作业,会浪费大量的时间,如果我们先把作业交给小组长,小组长再交给班长,由班长负责交给老师,这样可以节省很多工夫。
在Web
开发中,网络请求是很大的开销。假设我们在做一个文件同步的功能,当我们选中一个checkbox
的时候,它对应的文件就会被同步到另外一台备用服务器上面:
<input type="checkbox" id="1" />1 <input type="checkbox" id="2" />2
<input type="checkbox" id="3" />3 <input type="checkbox" id="4" />4
<input type="checkbox" id="5" />5 <input type="checkbox" id="6" />6
<input type="checkbox" id="7" />7 <input type="checkbox" id="8" />8
<input type="checkbox" id="9" />9
var synchronousFile = function (id) {
console.log("开始同步文件,id 为: " + id);
};
var checkbox = document.getElementsByTagName("input"); // 获取所有的checkbox
for (var i = 0, c; (c = checkbox[i++]); ) {
// 每点击checkbox一次,就发送一次文件
c.onclick = function () {
if (this.checked === true) {
synchronousFile(this.id);
}
};
}
当我们选中4个checkbox
的时候,依次往服务器发送了4次同步文件的请求。而点击一个checkbox
并不是很复杂的操作,用户可以再很短的时间内快速进行checkbox
的点击工作,由此可见,如此频繁的网络请求将会带来相当大的开销。
这时,我们可以通过一个代理函数proxySynchronousFile
来收集一段时间之内的请求,最后一次性发送给服务器。比如等待2秒之后才把这2秒之内需要同步的文件ID
打包发给服务器,如果不是对实时性要求非常高的系统,2 秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
var synchronousFile = function (id) {
console.log("开始同步文件,id 为: " + id);
};
var proxySynchronousFile = (function () {
var cache = [], // 保存2s之内需要同步的文件id
timer; // 定时器
return function (id) {
cache.push(id); // 推入id
if (timer) return; // 如果定时器已经启动,不再执行下面的操作
timer = setTimeout(() => {
synchronousFile(cache.join(",")); // 向服务器发送文件
clearTimeout(timer); // 清空定时器
timer = null;
cache.length = 0; // 清空id集合
}, 2000);
};
})();
var checkbox = document.getElementsByTagName("input"); // 获取所有的checkbox
for (var i = 0, c; (c = checkbox[i++]); ) {
// 每点击checkbox一次,就发送一次文件
c.onclick = function () {
if (this.checked === true) {
proxySynchronousFile(this.id);
}
};
}
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前一致,则可以直接返回前面存储的运算结果。
例如:计算乘积,假设计算乘积是一个很复杂的运算
先创建一个计算乘积的函数:
var mult = function () {
let result = 1;
for (let i = 0, l = arguments.length; i < l; i++) {
result = result * arguments[i];
}
return result;
};
mult( 2, 3 ); // 输出:6
mult( 2, 3, 4 ); // 输出:24
现在加入缓存代理函数:
var mult = function () {
let result = 1;
for (let i = 0, l = arguments.length; i < l; i++) {
result = result * arguments[i];
}
return result;
};
var proxyMult = (function () {
let cache = {};
return function () {
let 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
函数并没有被计算,直接返回了之前缓存好的计算结果。