JavaScript设计模式与开发实践(三)代理模式

代理模式

不方便直接访问某个对象时,使用代理对象来进行访问。

虚拟代理

预加载图片

假设我们有这样一个需求:我们需要加载很多图片,期望在图片被下载之前展示loading效果,当图片下载后展示图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const myImage = (function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);
const img = new Image()
img.onload = function (params) {
imgNode.src = this.src;
}
return {
setSrc: function (src) {
imgNode.src = './bg.jpg';
img.src = src
}
}
})();
myImage.setSrc("https://jing-jiu.github.io/jing-jiu/img/avatar.jpg");

对于我们来说,把图片加载出来是我们主要的目的,而做预加载,loading只是优化的操作,因此我们应该:

  1. 将预加载图片和设置图片的src这两个功能分离开来
  2. 同时我们期望不管是直接给图片设置src还是经过预加载,二者的调用方式是一致的。

对于下面的代码,当我们不需要预加载图片时,我们只需要将proxyImage替换为myImage。但是显然,代理模式的代码量比不使用代理模式会多很多,但是在大型项目中这样的设计是非常有必要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const myImage = (function () {
const imgNode = document.createElement('img');
document.body.appendChild(imgNode);

return {
setSrc: function (src) {
imgNode.src = src;
}
}
})();

const proxyImage = (function () {
const img = new Image;
img.onload = function () {
myImage.setSrc(this.src);
}
return {
setSrc: function (src) {
myImage.setSrc('loading.gif');
img.src = src;
}
}
})();
proxyImage.setSrc("https://jing-jiu.github.io/jing-jiu/img/avatar.jpg");

合并HTTP请求

假设我们在做一个文件同步的功能,当我们选中一个 checkbox 的时候,它对应的文件就会被同步到另外一台备用服务器上面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class="upload-item">
<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
</div>
<script>
const synchronousFile = function (id) {
console.log('开始同步文件,id 为: ' + id);
};
const checkbox = document.getElementsByTagName('input');
for (let i = 0, c; c = checkbox[i++];) {
c.onclick = function () {
if (this.checked === true) {
synchronousFile(this.id);
}
}
};
</script>

我们期望可以收集几秒内的所有选中文件,统一发请求给后端,这样可以大大减少请求的次数。(在对于实时性要求不高的场景完全是可行的)

1
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
35
36
37
38
39
40
41
42
43
44
45
46
<div class="upload-item">
<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
</div>
<script>
const synchronousFile = function (id) {
console.log('开始同步文件,id 为: ' + id);
};
const proxySynchronousFile = (function () {
let timer = null
const cache = new Set()

return (id) => {
cache.add(id)

if (timer) {
return
}

timer = setTimeout(() => {
synchronousFile([...cache].join(","))
clearTimeout(timer)
timer = null
cache.clear()
}, 2000)
}
})()

const checkbox = document.getElementsByTagName('input');
for (let i = 0, c; c = checkbox[i++];) {
c.onclick = function () {
for (let i = 0, c; c = checkbox[i++];) {
if (c.checked === true) {
proxySynchronousFile(c.id);
}
}
}
};
</script>

惰性加载

跟加载图片类似,有一些脚本是当我们打开控制台时才会执行,因此我们不需要一开始就加载这些脚本, 而是在用户按下F12打开控制台时加载脚本 。但是需要注意,我们一开始没有加载脚本,那么在代码中调用这些未加载的脚本的方法就会出现问题,因此我们需要:

  1. 一个代理对象,帮我们代理需要调用的方法,在脚本加载成功后执行脚本上的方法。
  2. 并且我们希望重复按下F12时脚本只被加载一次。
1
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
const proxyConsole = (function () {
const cache = []
const handler = function (e) {
if (e.keyCode === 113) {
const script = document.createElement("script")
script.onload = () => {
for (let i = 0, fn; fn = cache[i++];) {
fn()
}
}
script.src = 'remoteConsole.js'
document.getElementsByTagName("head")[0].appendChild(script)
document.body.removeEventListener("keydown", handler) // 只加载一次外部脚本
}
};
document.body.addEventListener("keydown", handler)
return {
log() {
const args = arguments;
// 外部脚本的方法
cache.push(() => {
return remoteConsole.log.apply(remoteConsole, args);
});
}
}
})()
proxyConsole.log('xxx'); // 开始打印 log
// remoteConsole.js 代码
remoteConsole = {
log: function () {
// 代码
console.log(Array.prototype.join.call(arguments));
}
};

缓存代理

有点类似于算法中的哈希表,将结果缓存起来,下次遇到同样的请求直接返回。

1
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;
};
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

其他代理模式

  • 防火墙代理:控制网络资源的访问,保护主题不让“坏人”接近。
  • 远程代理:为一个对象在不同的地址空间提供局部代表,在 Java 中,远程代理可以是另一个虚拟机中的对象。
  • 保护代理:用于对象应该有不同访问权限的情况。
  • 智能引用代理:取代了简单的指针,它在访问对象时执行一些附加操作,比如计算一个对象被引用的次数。
  • 写时复制代理:通常用于复制一个庞大对象的情况。写时复制代理延迟了复制的过程,当对象被真正修改时,才对它进行复制操作。写时复制代理是虚拟代理的一种变体,DLL (操作系统中的动态链接库)是其典型运用场景。

JavaScript设计模式与开发实践(三)代理模式
https://jing-jiu.github.io/jing-jiu/2023/01/06/notebooks/JavaScript设计模式与实践/设计模式(三)/
作者
Jing-Jiu
发布于
2023年1月6日
许可协议