第六章-代理模式
定义:当用户不方便直接访问一个对象时,提供一个代理对象来控制对这个对象的访问,客户先访问代理对象,然后由代理对象先处理后,再交由本体对象处理。例如:明星经纪人。
使用代理之前:
graph TD;
客户-->本体
2
使用代理之后:
graph TD;
客户-->代理对象-->本体
2
# 小明追女神的故事
小明喜欢一个女生,但是不便于直接向女生表白,于是打算借助两人的共同好友 B 来代替自己先送花增加好感。不用代理模式之前的代码可能是这样的:
var Flower = function () {};
var xiaoming = {
sendFlower: function (target) {
var flower = new Flower();
target.receiveFlower(flower);
},
};
var girl = {
receiveFlower: function (flower) {
console.log('收到花' + flower);
},
};
xiaoming.sendFlower(girl);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
然后,我们使用代理模式,来让 B 给女生送花
var Flower = function () {};
var xiaoming = {
sendFlower: function (target) {
var flower = new Flower();
target.receiveFlower(flower);
},
};
var B = {
receiveFlower: function (flower) {
girl.receiveFlower(flower);
},
};
var girl = {
receiveFlower: function (flower) {
console.log('收到花' + flower);
},
};
xiaoming.sendFlower(B);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
看起来上面的两段代码没什么不一样的,无非就是将花交由 B 中转了一下。但是如果我们把故事背景修改一下,女生在开心的时候收到花,小明表白的概率会比较大,而且 B 比较了解这个女生,所以小明把花交给 B 后,B 不是立马就送给女生,而是等到女生心情好的时候再送,那么代码会变成这样:
var Flower = function () {};
var xiaoming = {
sendFlower: function (target) {
var flower = new Flower();
target.receiveFlower(flower);
},
};
var B = {
receiveFlower: function (flower) {
girl.listentGoodMood(function () {
girl.receiveFlower(flower);
});
},
};
var girl = {
receiveFlower: function (flower) {
console.log('收到花' + flower);
},
listentGoodMood: function (fn) {
setTimeout(() => {
fn();
}, 3000); // 假设女生 3s 后心情变好
},
};
xiaoming.sendFlower(B);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 保护代理和虚拟代理
保护代理:可以过滤掉某些不合法的请求;
虚拟代理:将一些昂贵的操作交由代理对象再合适的时候再执行;
# 虚拟代理实现图片预加载
在 web 开发中,如果直接给某个 img 对象设置 src 属性,当图片较大时,则可能会出现一段时间的白屏。常见的做法是先用一张 loading 小图占位,然后用异步的方式加载图片,当图片加载完成后再把它填充到 img 上,这个场景就比较适合使用虚拟代理。
没有使用代理之前,我们的代码可能是这样写的:
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
},
};
})();
myImage.setSrc(
'https://cooljser-image.oss-cn-shanghai.aliyuncs.com/20201201204322.png'
);
2
3
4
5
6
7
8
9
10
11
12
13
14
如果我们将网速调成 slow 模式,这时候会发现页面会有一段时间的白屏。下面我们使用代理模式来进行优化:
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
return {
setSrc: function (src) {
imgNode.src = src;
},
};
})();
var proxyImage = (function () {
var img = new Image();
img.onload = function () {
myImage.setSrc(this.src);
};
return {
setSrc: function (src) {
myImage.setSrc(
'file:///C:/Users/admin/Downloads/Misc/5-121204193Q8.gif'
);
img.src = src;
},
};
})();
proxyImage.setSrc(
'https://cooljser-image.oss-cn-shanghai.aliyuncs.com/20201201204322.png'
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
现在我们通过 proxyImage 间接的访问 myImage。proxyImage 控制了用户对 myImage 的访问,并且在此过程中加入了一些额外的操作,比如在真正的图片加载好之前,先把 img 节点的 src 设置成一张本地的 loading 图片。
# 代理的意义
看到这里,可能有人会问,上面的功能我不用代理模式也一样能实现,那么使用代理模式的意义到底是什么呢?确实,如果不使用代理模式的话,上面的功能我们也可以这么来实现:
var myImage = (function () {
var imgNode = document.createElement('img');
document.body.appendChild(imgNode);
var img = new Image();
img.onload = function () {
imgNode.src = img.src;
};
return {
setSrc: function (src) {
imgNode.src =
'file:///C:/Users/admin/Downloads/Misc/5-121204193Q8.gif';
img.src = src;
},
};
})();
myImage.setSrc(
'https://cooljser-image.oss-cn-shanghai.aliyuncs.com/20201201204322.png'
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
虽然上面的代码也实现了预加载的功能,但是却违背了"单一职责原则"。myImage 对象除了负责给 img 节点设置 src 外,还要负责预加载图片。如果我们只是希望从网络上获取一些体积很小的图片,又或者几年后网速快到根本不需要预加载,那么我们需要把预加载相关的代码从 myImage 对象里删掉,这时候就不得不对 myImage 进行改动了。
实际上,我们需要的只是给 img 节点设置 src,预加载图片只是一个优化的功能。如果能把这个操作放到另一个对象上,自然是一个非常好的方法。于是代理的左右在这里就体现出来了,代理负责预加载图片,预加载的操作完成之后,把请求重新交给本体 myImage 对象。
使用代理后,我们并没有改变或者增加 myImage 的接口,但是通过代理对象,实际上给 myImage 添加了新的行为。这是符合开放-封闭原则的。给 img 节点设置 src 和图片预加载这两个功能,被隔离在这两个对象里,它们可以各自变化而不影响对方。即使有一天我们不需要使用预加载功能了,那么只需要改成直接请求本体对象即可。
# 虚拟代理合并 HTTP 请求
假设我们现在有这样一个场景:在 web 开发中,我们要做一个文件同步的功能,用户选中一个 checkbox 的时候,就将选中的文件同步到另一台服务器上。如果我们一次选中 3 个文件,那么将会产生 3 个同步请求,这显然有些浪费。于是我们可以对请求进行合并,比如选中一个文件后等待 2s 再同步,示例代码如下:
var synchronousFile = function (id) {
console.log('开始同步文件, id 为:' + id);
};
var proxySynchronousFile = (function () {
var cache = [],
timer;
return function (id) {
cache.push(id);
if (timer) {
return;
}
timer = setTimeout(() => {
synchronousFile(cache.join(','));
clearTimeout(timer);
timer = null;
cache.length = 0;
}, 2000);
};
})();
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 缓存代理
我们来看一个计算乘积的例子:
var mult = function () {
console.log('开始计算乘积:');
var a = 1;
for (var 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
2
3
4
5
6
7
8
9
10
11
现在加入缓存代理函数:
var proxyMult = (function () {
var cache = [];
return function () {
var args = Array.prototype.join.call(arguments, ',');
if (args in cache) {
console.log('from cache')
return cache[args];
}
return (cache[args] = mult.apply(this, arguments));
};
})();
console.log(proxyMult(2, 3)); // 6
console.log(proxyMult(2, 3)); // from cache 6
2
3
4
5
6
7
8
9
10
11
12
13
14
# 利用高阶函数动态创建代理
通过传入高阶函数的方式,我们可以为各种计算方法创建缓存代理。现在这些计算方法被当做参数传入一个专门用于创建缓存对象的工厂中,这样一来,我们就可以为乘法、加法、减法等创建缓存代理,代码如下:
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) {
console.log('form cache')
return cache[args];
}
return (cache[args] = fn.apply(this, arguments));
};
};
var proxyMult = createProxyFactory(mult);
var proxyPlus = createProxyFactory(plus);
console.log(proxyMult(2, 3)); // 6
console.log(proxyMult(2, 3)); // from cache 6
console.log(proxyPlus(2, 3)); // 5
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34