小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
记录自己学习设计模式,内容来自
《JavaScript设计模式与开发实践》
代理模式的定义
代理模式是为一个对象提供一个代用品或占位符,以便控制对它的访问
代理模式的关键是,当客户不方便直接访问一个对象或者不满足需要的时候,提供一个替身对象来控制对这个对象的访问,客户实际上访问的是替身对象。替身对象对请求做出一些处理之后,再把请求转交给本地对象
小明追MM
小明喜欢女孩A,小明决定送A一束花表白,因为小明害羞不敢直接送,小明决定让A的朋友B代替自己完成送花这件事
先看看不用代理模式的情况
var Flower = function() {}
var xiaoming = {
sendFlower(target) {
const flower = new Flower()
target.receiveFlower(flower)
}
}
var A = {
receiveFlower(flower) {
console.log('收到花', flower)
}
}
// 接下来我们引入代理B
var B = {
receiveFlower(flower) {
A.receiveFlower(flower)
}
}
xiaoming.sendFlower(B)
保护代理和虚拟的代理
假设现实世界中花价格不菲,导致在程序世界里,new Flower也是一个代价昂贵的操作,那么我们可以把new Flower的操作交给代理B去执行,代理B会选择在A心情好时再执行new Flower,这是代理模式的另一种形式,叫虚拟代理。虚拟代理会把一些开销很大的对象,延迟到真正需要它的时候才去创建。代码如下
var B = {
receiveFlower(flower) {
A.listenGoodMood(() => {
const flower = new Flower()
A.receiveFlower(flower)
})
}
}
虚拟代理实现图片预加载
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const myImage = (() => {
var imgNode = document.createElement('img')
document.body.appendChild(imgNode)
return {
setSrc(src) {
imgNode.src = src
}
}
})()
var proxyImage = (() => {
const img = new Image;
img.onload = function() {
myImage.setSrc(this.src)
}
return {
setSrc(src) {
myImage.setSrc('./assets/loading.gif')
img.src = src
}
}
})()
proxyImage.setSrc('./assets/hashiqi.webp')
</script>
</body>
</html>
虚拟代理合并HTTP请求
例如有如下场景:每周我们要写一份工作周报,周报需要给总监批阅。总监手下管理着150个员工,如果我们每个人直接把周报发给总监,那总监可能要把一整周的时候都花在查看周报上面。
现在我们把周报发给各自的组长,组长作为代理,把组内成员的周报合提炼成一份后一次性发给总监。这样一来,总监的邮箱便清净来了
这个例子再程序世界里很容易引起共鸣,在Web开发中,也许最大的开销就是网络请求。假设我们在做一个文件同步的功能,当我们选中一个checkbox的时候,它对应的文件就会被同步到另外到另外一台备用服务器上面
const synchronousFile = function(id) {
console.log('开始同步文件,id为:', + id)
}
var checkbox = document.getElementsByTagName('input')
for(let i = 0, c; c = checkbox[i++];) {
c.onclick = function() {
if (this.checked === true) {
synchronousFile(this.id)
}
}
}
当我们选中三个checkbox的时候,依次往服务器发送了3次同步文件的请求。而点击一个checkbox并不是很复杂的操作。可以预见,如此频繁的网络请求将会带来相当大的开销。
解决方案时,我们通过一个代理函数proxySynchronousFile来收集一段时间之内的请求,最后一次性发送给服务器。比如我们等待两秒之后才把这2秒之内需要同步的文件ID打包发送给服务器,如果不是对实时性要求非常高的系统,2秒的延迟不会带来太大副作用,却能大大减轻服务器的压力。
虚拟加载在惰性单例中的应用
例如有一个插件叫miniConsole.log,按F2可以打开自定义控制台
例如在控制台输入miniConsole.log(1)
这句话会在页面上创建一个div,并且把log显示在div里面。
miniConsole的代码量大概有1000行,也许我们并不想一开始就加载这么大的js文件,因为也许并不是每个用户都需要打印log。我们希望在有必要的时候才开始加载它,比如当用户按下F2来主动唤出控制台的时候。
在miniConsole.js加载之前,为了能够让用户正常的使用里面的API,通常我们的解决方案时用一个展位的miniConsole代理来给用户提前使用。
const miniConsole = (function() {
const cache = []
const handler = function(ev) {
if (ev.keyCode === 112) {
const script = document.createElement('script')
script.onload = function() {
for(let i = 0, fn; fn = cache[i++];) {
fn()
}
};
script.src = 'miniConsole.js'
document.getElementsByClassName('head')[0].appendChild(script)
document.body.removeEventListener('keydown', handler)
}
}
document.body.addEventListener('keydown', handler, false)
return {
log() {
const args = arguments
cache.push(() => {
return miniConsole.apply(miniConsole, args)
})
}
}
})
miniConsole.log(11) // 开始打印log
miniConsole = {
log() {
// 真正的代码略
}
}
缓存代理
缓存代理可以为一些开销大的运算结果提供暂时的存储,在下次运算时,如果传递进来的参数跟之前的一致,则可以直接返回前面存储的运算结果
例子1:计算乘积
const mult = function() {
let a = 1
for(let i = 0, l = arguments.length; i < l; i ++) {
a = a * arguments[i]
}
return a
}
console.log(mult(2, 3)) // 6
console.log(mult(2, 3, 4)) // 24
// 现在加入缓存代理函数
const proxyMult = (function() {
const cache = {}
return function() {
const args = Array.prototype.join.call(arguments, ',')
if (args in cache) {
return cache[args]
}
return cache[args] = mult.apply(this, arguments)
}
})()
console.log(proxyMult(1, 2, 3, 4))
console.log(proxyMult(1, 2, 3, 4))
利用高阶函数创建代理
// 计算乘积
const mult = function() {
let a = 1
for(let i = 0, l = arguments.length; i < l; i ++) {
a = a * arguments[i]
}
return a
}
// 计算加和
const plus = function() {
let a = 0
for(let i = 0, l = arguments.length; i < l; i++) {
a = a + arguments[i]
}
return a
}
// 创建缓存代理的工厂
const createProxyFactory = function(fn) {
const cache = {}
return function() {
const args = Array.prototype.join.call(arguments, ',')
if (args in cache) {
return cache[args]
}
console.log('计算');
return cache[args] = fn.apply(this, arguments)
}
}
const proxyMult = createProxyFactory(mult)
const proxyPlus = createProxyFactory(plus)
console.log(proxyMult(1, 2, 3, 4));
console.log(proxyMult(1, 2, 3, 4));
console.log(proxyPlus(1, 2, 3, 4));
console.log(proxyPlus(1, 2, 3, 4));
其他代理模式
防火墙代理:控制网络资源的访问,保护主机不让"坏人"接近
远程代理:为一个对象在不同的地址空间提供局部代理,在Java中,远程代理可以是另一个虚拟机中的对象
保护代理:用于对象应该有不同访问权限的情况
智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL(操作系统中的动态链接库)是典型运用场景